mirror of
https://github.com/home-assistant/core.git
synced 2025-09-21 02:49:32 +00:00
Compare commits
1 Commits
2024.6.0b4
...
bump-openc
Author | SHA1 | Date | |
---|---|---|---|
![]() |
628cc5dccb |
@@ -137,7 +137,6 @@ tests: &tests
|
||||
- tests/syrupy.py
|
||||
- tests/test_util/**
|
||||
- tests/testing_config/**
|
||||
- tests/typing.py
|
||||
- tests/util/**
|
||||
|
||||
other: &other
|
||||
|
240
.coveragerc
240
.coveragerc
@@ -1,17 +1,12 @@
|
||||
# Sorted by hassfest.
|
||||
#
|
||||
# To sort, run python3 -m script.hassfest -p coverage
|
||||
|
||||
[run]
|
||||
source = homeassistant
|
||||
omit =
|
||||
homeassistant/__main__.py
|
||||
homeassistant/helpers/backports/aiohttp_resolver.py
|
||||
homeassistant/helpers/signal.py
|
||||
homeassistant/scripts/__init__.py
|
||||
homeassistant/scripts/benchmark/__init__.py
|
||||
homeassistant/scripts/check_config.py
|
||||
homeassistant/scripts/ensure_config.py
|
||||
homeassistant/scripts/benchmark/__init__.py
|
||||
homeassistant/scripts/macos/__init__.py
|
||||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
@@ -58,19 +53,13 @@ omit =
|
||||
homeassistant/components/airvisual/sensor.py
|
||||
homeassistant/components/airvisual_pro/__init__.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/alarm_control_panel.py
|
||||
homeassistant/components/alarmdecoder/binary_sensor.py
|
||||
homeassistant/components/alarmdecoder/entity.py
|
||||
homeassistant/components/alarmdecoder/sensor.py
|
||||
homeassistant/components/alpha_vantage/sensor.py
|
||||
homeassistant/components/amazon_polly/*
|
||||
homeassistant/components/ambiclimate/climate.py
|
||||
homeassistant/components/ambient_station/__init__.py
|
||||
homeassistant/components/ambient_station/binary_sensor.py
|
||||
homeassistant/components/ambient_station/entity.py
|
||||
@@ -88,9 +77,6 @@ omit =
|
||||
homeassistant/components/aprilaire/climate.py
|
||||
homeassistant/components/aprilaire/coordinator.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/aquostv/media_player.py
|
||||
homeassistant/components/arcam_fmj/__init__.py
|
||||
@@ -117,8 +103,10 @@ omit =
|
||||
homeassistant/components/aurora/sensor.py
|
||||
homeassistant/components/avea/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/awair/coordinator.py
|
||||
homeassistant/components/baf/__init__.py
|
||||
homeassistant/components/baf/climate.py
|
||||
homeassistant/components/baf/entity.py
|
||||
@@ -129,6 +117,7 @@ omit =
|
||||
homeassistant/components/baf/switch.py
|
||||
homeassistant/components/baidu/tts.py
|
||||
homeassistant/components/bang_olufsen/__init__.py
|
||||
homeassistant/components/bang_olufsen/const.py
|
||||
homeassistant/components/bang_olufsen/entity.py
|
||||
homeassistant/components/bang_olufsen/media_player.py
|
||||
homeassistant/components/bang_olufsen/util.py
|
||||
@@ -200,8 +189,9 @@ omit =
|
||||
homeassistant/components/comelit/__init__.py
|
||||
homeassistant/components/comelit/alarm_control_panel.py
|
||||
homeassistant/components/comelit/climate.py
|
||||
homeassistant/components/comelit/coordinator.py
|
||||
homeassistant/components/comelit/const.py
|
||||
homeassistant/components/comelit/cover.py
|
||||
homeassistant/components/comelit/coordinator.py
|
||||
homeassistant/components/comelit/humidifier.py
|
||||
homeassistant/components/comelit/light.py
|
||||
homeassistant/components/comelit/sensor.py
|
||||
@@ -212,7 +202,6 @@ omit =
|
||||
homeassistant/components/control4/__init__.py
|
||||
homeassistant/components/control4/director_utils.py
|
||||
homeassistant/components/control4/light.py
|
||||
homeassistant/components/control4/media_player.py
|
||||
homeassistant/components/coolmaster/coordinator.py
|
||||
homeassistant/components/cppm_tracker/device_tracker.py
|
||||
homeassistant/components/crownstone/__init__.py
|
||||
@@ -249,8 +238,8 @@ omit =
|
||||
homeassistant/components/dominos/*
|
||||
homeassistant/components/doods/*
|
||||
homeassistant/components/doorbird/__init__.py
|
||||
homeassistant/components/doorbird/button.py
|
||||
homeassistant/components/doorbird/camera.py
|
||||
homeassistant/components/doorbird/button.py
|
||||
homeassistant/components/doorbird/device.py
|
||||
homeassistant/components/doorbird/entity.py
|
||||
homeassistant/components/doorbird/util.py
|
||||
@@ -261,18 +250,22 @@ omit =
|
||||
homeassistant/components/dormakaba_dkey/lock.py
|
||||
homeassistant/components/dormakaba_dkey/sensor.py
|
||||
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/dublin_bus_transport/sensor.py
|
||||
homeassistant/components/dunehd/__init__.py
|
||||
homeassistant/components/dunehd/media_player.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/light.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/sensor.py
|
||||
homeassistant/components/dweet/*
|
||||
@@ -311,12 +304,10 @@ omit =
|
||||
homeassistant/components/edl21/__init__.py
|
||||
homeassistant/components/edl21/sensor.py
|
||||
homeassistant/components/egardia/*
|
||||
homeassistant/components/electrasmart/__init__.py
|
||||
homeassistant/components/electrasmart/climate.py
|
||||
homeassistant/components/electric_kiwi/__init__.py
|
||||
homeassistant/components/electric_kiwi/api.py
|
||||
homeassistant/components/electric_kiwi/coordinator.py
|
||||
homeassistant/components/electric_kiwi/oauth2.py
|
||||
homeassistant/components/electric_kiwi/coordinator.py
|
||||
homeassistant/components/electric_kiwi/select.py
|
||||
homeassistant/components/eliqonline/sensor.py
|
||||
homeassistant/components/elkm1/__init__.py
|
||||
@@ -329,7 +320,8 @@ omit =
|
||||
homeassistant/components/elmax/__init__.py
|
||||
homeassistant/components/elmax/alarm_control_panel.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/switch.py
|
||||
homeassistant/components/elv/*
|
||||
@@ -363,17 +355,12 @@ omit =
|
||||
homeassistant/components/environment_canada/weather.py
|
||||
homeassistant/components/envisalink/*
|
||||
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/coordinator.py
|
||||
homeassistant/components/epion/sensor.py
|
||||
homeassistant/components/epson/__init__.py
|
||||
homeassistant/components/epson/media_player.py
|
||||
homeassistant/components/eq3btsmart/__init__.py
|
||||
homeassistant/components/eq3btsmart/climate.py
|
||||
homeassistant/components/eq3btsmart/entity.py
|
||||
homeassistant/components/eq3btsmart/models.py
|
||||
homeassistant/components/epsonworkforce/sensor.py
|
||||
homeassistant/components/escea/__init__.py
|
||||
homeassistant/components/escea/climate.py
|
||||
homeassistant/components/escea/discovery.py
|
||||
@@ -388,11 +375,11 @@ omit =
|
||||
homeassistant/components/ezviz/binary_sensor.py
|
||||
homeassistant/components/ezviz/button.py
|
||||
homeassistant/components/ezviz/camera.py
|
||||
homeassistant/components/ezviz/coordinator.py
|
||||
homeassistant/components/ezviz/entity.py
|
||||
homeassistant/components/ezviz/image.py
|
||||
homeassistant/components/ezviz/light.py
|
||||
homeassistant/components/ezviz/coordinator.py
|
||||
homeassistant/components/ezviz/number.py
|
||||
homeassistant/components/ezviz/entity.py
|
||||
homeassistant/components/ezviz/select.py
|
||||
homeassistant/components/ezviz/sensor.py
|
||||
homeassistant/components/ezviz/siren.py
|
||||
@@ -463,8 +450,8 @@ omit =
|
||||
homeassistant/components/freebox/camera.py
|
||||
homeassistant/components/freebox/home_base.py
|
||||
homeassistant/components/freebox/switch.py
|
||||
homeassistant/components/fritz/coordinator.py
|
||||
homeassistant/components/fritz/entity.py
|
||||
homeassistant/components/fritz/common.py
|
||||
homeassistant/components/fritz/device_tracker.py
|
||||
homeassistant/components/fritz/services.py
|
||||
homeassistant/components/fritz/switch.py
|
||||
homeassistant/components/fritzbox_callmonitor/__init__.py
|
||||
@@ -502,6 +489,7 @@ omit =
|
||||
homeassistant/components/gpsd/sensor.py
|
||||
homeassistant/components/greenwave/light.py
|
||||
homeassistant/components/growatt_server/__init__.py
|
||||
homeassistant/components/growatt_server/const.py
|
||||
homeassistant/components/growatt_server/sensor.py
|
||||
homeassistant/components/growatt_server/sensor_types/*
|
||||
homeassistant/components/gstreamer/media_player.py
|
||||
@@ -515,7 +503,6 @@ omit =
|
||||
homeassistant/components/guardian/util.py
|
||||
homeassistant/components/guardian/valve.py
|
||||
homeassistant/components/habitica/__init__.py
|
||||
homeassistant/components/habitica/coordinator.py
|
||||
homeassistant/components/habitica/sensor.py
|
||||
homeassistant/components/harman_kardon_avr/media_player.py
|
||||
homeassistant/components/harmony/data.py
|
||||
@@ -537,13 +524,16 @@ omit =
|
||||
homeassistant/components/hive/switch.py
|
||||
homeassistant/components/hive/water_heater.py
|
||||
homeassistant/components/hko/__init__.py
|
||||
homeassistant/components/hko/coordinator.py
|
||||
homeassistant/components/hko/weather.py
|
||||
homeassistant/components/hko/coordinator.py
|
||||
homeassistant/components/hlk_sw16/__init__.py
|
||||
homeassistant/components/hlk_sw16/switch.py
|
||||
homeassistant/components/home_connect/__init__.py
|
||||
homeassistant/components/home_connect/api.py
|
||||
homeassistant/components/home_connect/binary_sensor.py
|
||||
homeassistant/components/home_connect/entity.py
|
||||
homeassistant/components/home_connect/light.py
|
||||
homeassistant/components/home_connect/sensor.py
|
||||
homeassistant/components/home_connect/switch.py
|
||||
homeassistant/components/homematic/__init__.py
|
||||
homeassistant/components/homematic/binary_sensor.py
|
||||
@@ -555,6 +545,10 @@ omit =
|
||||
homeassistant/components/homematic/notify.py
|
||||
homeassistant/components/homematic/sensor.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/hp_ilo/sensor.py
|
||||
homeassistant/components/huawei_lte/__init__.py
|
||||
@@ -573,9 +567,9 @@ omit =
|
||||
homeassistant/components/hunterdouglas_powerview/sensor.py
|
||||
homeassistant/components/hunterdouglas_powerview/shade_data.py
|
||||
homeassistant/components/hunterdouglas_powerview/util.py
|
||||
homeassistant/components/hvv_departures/__init__.py
|
||||
homeassistant/components/huum/__init__.py
|
||||
homeassistant/components/huum/climate.py
|
||||
homeassistant/components/hvv_departures/__init__.py
|
||||
homeassistant/components/hvv_departures/binary_sensor.py
|
||||
homeassistant/components/hvv_departures/sensor.py
|
||||
homeassistant/components/ialarm/alarm_control_panel.py
|
||||
@@ -668,9 +662,9 @@ omit =
|
||||
homeassistant/components/keyboard/*
|
||||
homeassistant/components/keyboard_remote/*
|
||||
homeassistant/components/keymitt_ble/__init__.py
|
||||
homeassistant/components/keymitt_ble/coordinator.py
|
||||
homeassistant/components/keymitt_ble/entity.py
|
||||
homeassistant/components/keymitt_ble/switch.py
|
||||
homeassistant/components/keymitt_ble/coordinator.py
|
||||
homeassistant/components/kitchen_sink/weather.py
|
||||
homeassistant/components/kiwi/lock.py
|
||||
homeassistant/components/kodi/__init__.py
|
||||
@@ -681,7 +675,6 @@ omit =
|
||||
homeassistant/components/konnected/panel.py
|
||||
homeassistant/components/konnected/switch.py
|
||||
homeassistant/components/kostal_plenticore/__init__.py
|
||||
homeassistant/components/kostal_plenticore/coordinator.py
|
||||
homeassistant/components/kostal_plenticore/helper.py
|
||||
homeassistant/components/kostal_plenticore/select.py
|
||||
homeassistant/components/kostal_plenticore/sensor.py
|
||||
@@ -729,6 +722,7 @@ omit =
|
||||
homeassistant/components/lookin/sensor.py
|
||||
homeassistant/components/loqed/sensor.py
|
||||
homeassistant/components/luci/device_tracker.py
|
||||
homeassistant/components/luftdaten/sensor.py
|
||||
homeassistant/components/lupusec/__init__.py
|
||||
homeassistant/components/lupusec/alarm_control_panel.py
|
||||
homeassistant/components/lupusec/binary_sensor.py
|
||||
@@ -738,7 +732,6 @@ omit =
|
||||
homeassistant/components/lutron/binary_sensor.py
|
||||
homeassistant/components/lutron/cover.py
|
||||
homeassistant/components/lutron/entity.py
|
||||
homeassistant/components/lutron/event.py
|
||||
homeassistant/components/lutron/fan.py
|
||||
homeassistant/components/lutron/light.py
|
||||
homeassistant/components/lutron/switch.py
|
||||
@@ -754,11 +747,11 @@ omit =
|
||||
homeassistant/components/lyric/climate.py
|
||||
homeassistant/components/lyric/sensor.py
|
||||
homeassistant/components/mailgun/notify.py
|
||||
homeassistant/components/map/*
|
||||
homeassistant/components/mastodon/notify.py
|
||||
homeassistant/components/matrix/__init__.py
|
||||
homeassistant/components/matrix/notify.py
|
||||
homeassistant/components/matter/__init__.py
|
||||
homeassistant/components/matter/fan.py
|
||||
homeassistant/components/meater/__init__.py
|
||||
homeassistant/components/meater/sensor.py
|
||||
homeassistant/components/medcom_ble/__init__.py
|
||||
@@ -783,17 +776,15 @@ omit =
|
||||
homeassistant/components/microbees/__init__.py
|
||||
homeassistant/components/microbees/api.py
|
||||
homeassistant/components/microbees/application_credentials.py
|
||||
homeassistant/components/microbees/binary_sensor.py
|
||||
homeassistant/components/microbees/button.py
|
||||
homeassistant/components/microbees/climate.py
|
||||
homeassistant/components/microbees/const.py
|
||||
homeassistant/components/microbees/coordinator.py
|
||||
homeassistant/components/microbees/cover.py
|
||||
homeassistant/components/microbees/entity.py
|
||||
homeassistant/components/microbees/light.py
|
||||
homeassistant/components/microbees/sensor.py
|
||||
homeassistant/components/microbees/switch.py
|
||||
homeassistant/components/microsoft/tts.py
|
||||
homeassistant/components/mikrotik/coordinator.py
|
||||
homeassistant/components/mikrotik/hub.py
|
||||
homeassistant/components/mill/climate.py
|
||||
homeassistant/components/mill/sensor.py
|
||||
homeassistant/components/minio/minio_helper.py
|
||||
@@ -804,20 +795,15 @@ omit =
|
||||
homeassistant/components/mochad/switch.py
|
||||
homeassistant/components/modem_callerid/button.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/coordinator.py
|
||||
homeassistant/components/monzo/__init__.py
|
||||
homeassistant/components/monzo/api.py
|
||||
homeassistant/components/moehlenhoff_alpha2/sensor.py
|
||||
homeassistant/components/motion_blinds/__init__.py
|
||||
homeassistant/components/motion_blinds/coordinator.py
|
||||
homeassistant/components/motion_blinds/cover.py
|
||||
homeassistant/components/motion_blinds/entity.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/binary_sensor.py
|
||||
homeassistant/components/motionmount/entity.py
|
||||
@@ -843,15 +829,8 @@ omit =
|
||||
homeassistant/components/mysensors/switch.py
|
||||
homeassistant/components/mystrom/binary_sensor.py
|
||||
homeassistant/components/mystrom/light.py
|
||||
homeassistant/components/mystrom/sensor.py
|
||||
homeassistant/components/mystrom/switch.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
|
||||
homeassistant/components/mystrom/sensor.py
|
||||
homeassistant/components/nad/media_player.py
|
||||
homeassistant/components/nanoleaf/__init__.py
|
||||
homeassistant/components/nanoleaf/button.py
|
||||
@@ -859,13 +838,13 @@ omit =
|
||||
homeassistant/components/nanoleaf/light.py
|
||||
homeassistant/components/neato/__init__.py
|
||||
homeassistant/components/neato/api.py
|
||||
homeassistant/components/neato/button.py
|
||||
homeassistant/components/neato/camera.py
|
||||
homeassistant/components/neato/entity.py
|
||||
homeassistant/components/neato/hub.py
|
||||
homeassistant/components/neato/sensor.py
|
||||
homeassistant/components/neato/switch.py
|
||||
homeassistant/components/neato/vacuum.py
|
||||
homeassistant/components/neato/button.py
|
||||
homeassistant/components/nederlandse_spoorwegen/sensor.py
|
||||
homeassistant/components/netdata/sensor.py
|
||||
homeassistant/components/netgear/__init__.py
|
||||
@@ -917,8 +896,9 @@ omit =
|
||||
homeassistant/components/notion/util.py
|
||||
homeassistant/components/nsw_fuel_station/sensor.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/sensor.py
|
||||
homeassistant/components/nx584/alarm_control_panel.py
|
||||
homeassistant/components/oasa_telematics/sensor.py
|
||||
homeassistant/components/obihai/__init__.py
|
||||
@@ -931,12 +911,11 @@ omit =
|
||||
homeassistant/components/ohmconnect/sensor.py
|
||||
homeassistant/components/ombi/*
|
||||
homeassistant/components/omnilogic/__init__.py
|
||||
homeassistant/components/omnilogic/coordinator.py
|
||||
homeassistant/components/omnilogic/common.py
|
||||
homeassistant/components/omnilogic/sensor.py
|
||||
homeassistant/components/omnilogic/switch.py
|
||||
homeassistant/components/ondilo_ico/__init__.py
|
||||
homeassistant/components/ondilo_ico/api.py
|
||||
homeassistant/components/ondilo_ico/coordinator.py
|
||||
homeassistant/components/ondilo_ico/sensor.py
|
||||
homeassistant/components/onkyo/media_player.py
|
||||
homeassistant/components/onvif/__init__.py
|
||||
@@ -948,6 +927,7 @@ omit =
|
||||
homeassistant/components/onvif/sensor.py
|
||||
homeassistant/components/onvif/util.py
|
||||
homeassistant/components/open_meteo/weather.py
|
||||
homeassistant/components/opencv/*
|
||||
homeassistant/components/openevse/sensor.py
|
||||
homeassistant/components/openexchangerates/__init__.py
|
||||
homeassistant/components/openexchangerates/coordinator.py
|
||||
@@ -959,6 +939,7 @@ omit =
|
||||
homeassistant/components/opengarage/sensor.py
|
||||
homeassistant/components/openhardwaremonitor/sensor.py
|
||||
homeassistant/components/openhome/__init__.py
|
||||
homeassistant/components/openhome/const.py
|
||||
homeassistant/components/openhome/media_player.py
|
||||
homeassistant/components/opensensemap/air_quality.py
|
||||
homeassistant/components/opentherm_gw/__init__.py
|
||||
@@ -969,22 +950,18 @@ omit =
|
||||
homeassistant/components/openuv/binary_sensor.py
|
||||
homeassistant/components/openuv/coordinator.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/weather.py
|
||||
homeassistant/components/openweathermap/weather_update_coordinator.py
|
||||
homeassistant/components/opnsense/__init__.py
|
||||
homeassistant/components/opnsense/device_tracker.py
|
||||
homeassistant/components/opower/__init__.py
|
||||
homeassistant/components/opower/coordinator.py
|
||||
homeassistant/components/opower/sensor.py
|
||||
homeassistant/components/opnsense/device_tracker.py
|
||||
homeassistant/components/opple/light.py
|
||||
homeassistant/components/oru/*
|
||||
homeassistant/components/orvibo/switch.py
|
||||
homeassistant/components/osoenergy/__init__.py
|
||||
homeassistant/components/osoenergy/binary_sensor.py
|
||||
homeassistant/components/osoenergy/sensor.py
|
||||
homeassistant/components/osoenergy/const.py
|
||||
homeassistant/components/osoenergy/water_heater.py
|
||||
homeassistant/components/osramlightify/light.py
|
||||
homeassistant/components/otp/sensor.py
|
||||
@@ -1020,7 +997,6 @@ omit =
|
||||
homeassistant/components/permobil/entity.py
|
||||
homeassistant/components/permobil/sensor.py
|
||||
homeassistant/components/philips_js/__init__.py
|
||||
homeassistant/components/philips_js/coordinator.py
|
||||
homeassistant/components/philips_js/light.py
|
||||
homeassistant/components/philips_js/media_player.py
|
||||
homeassistant/components/philips_js/remote.py
|
||||
@@ -1029,6 +1005,7 @@ omit =
|
||||
homeassistant/components/picotts/tts.py
|
||||
homeassistant/components/pilight/base_class.py
|
||||
homeassistant/components/pilight/binary_sensor.py
|
||||
homeassistant/components/pilight/const.py
|
||||
homeassistant/components/pilight/light.py
|
||||
homeassistant/components/pilight/switch.py
|
||||
homeassistant/components/ping/__init__.py
|
||||
@@ -1047,6 +1024,11 @@ omit =
|
||||
homeassistant/components/point/alarm_control_panel.py
|
||||
homeassistant/components/point/binary_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/progettihwsw/__init__.py
|
||||
homeassistant/components/progettihwsw/binary_sensor.py
|
||||
@@ -1073,12 +1055,12 @@ omit =
|
||||
homeassistant/components/quantum_gateway/device_tracker.py
|
||||
homeassistant/components/qvr_pro/*
|
||||
homeassistant/components/rabbitair/__init__.py
|
||||
homeassistant/components/rabbitair/const.py
|
||||
homeassistant/components/rabbitair/coordinator.py
|
||||
homeassistant/components/rabbitair/entity.py
|
||||
homeassistant/components/rabbitair/fan.py
|
||||
homeassistant/components/rachio/__init__.py
|
||||
homeassistant/components/rachio/binary_sensor.py
|
||||
homeassistant/components/rachio/coordinator.py
|
||||
homeassistant/components/rachio/device.py
|
||||
homeassistant/components/rachio/entity.py
|
||||
homeassistant/components/rachio/switch.py
|
||||
@@ -1095,12 +1077,22 @@ omit =
|
||||
homeassistant/components/rainmachine/__init__.py
|
||||
homeassistant/components/rainmachine/binary_sensor.py
|
||||
homeassistant/components/rainmachine/button.py
|
||||
homeassistant/components/rainmachine/coordinator.py
|
||||
homeassistant/components/rainmachine/select.py
|
||||
homeassistant/components/rainmachine/sensor.py
|
||||
homeassistant/components/rainmachine/switch.py
|
||||
homeassistant/components/rainmachine/update.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/recollect_waste/sensor.py
|
||||
homeassistant/components/recorder/repack.py
|
||||
@@ -1115,16 +1107,6 @@ omit =
|
||||
homeassistant/components/rejseplanen/sensor.py
|
||||
homeassistant/components/remember_the_milk/__init__.py
|
||||
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/button.py
|
||||
homeassistant/components/reolink/camera.py
|
||||
@@ -1150,10 +1132,7 @@ omit =
|
||||
homeassistant/components/roborock/coordinator.py
|
||||
homeassistant/components/rocketchat/notify.py
|
||||
homeassistant/components/romy/__init__.py
|
||||
homeassistant/components/romy/binary_sensor.py
|
||||
homeassistant/components/romy/coordinator.py
|
||||
homeassistant/components/romy/entity.py
|
||||
homeassistant/components/romy/sensor.py
|
||||
homeassistant/components/romy/vacuum.py
|
||||
homeassistant/components/roomba/__init__.py
|
||||
homeassistant/components/roomba/binary_sensor.py
|
||||
@@ -1168,18 +1147,18 @@ omit =
|
||||
homeassistant/components/roon/media_player.py
|
||||
homeassistant/components/roon/server.py
|
||||
homeassistant/components/route53/*
|
||||
homeassistant/components/rova/sensor.py
|
||||
homeassistant/components/rpi_camera/*
|
||||
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/bluetooth.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/coordinator.py
|
||||
homeassistant/components/rympro/sensor.py
|
||||
homeassistant/components/sabnzbd/__init__.py
|
||||
homeassistant/components/sabnzbd/coordinator.py
|
||||
homeassistant/components/sabnzbd/sensor.py
|
||||
homeassistant/components/saj/sensor.py
|
||||
homeassistant/components/satel_integra/*
|
||||
@@ -1187,10 +1166,12 @@ omit =
|
||||
homeassistant/components/screenlogic/binary_sensor.py
|
||||
homeassistant/components/screenlogic/climate.py
|
||||
homeassistant/components/screenlogic/coordinator.py
|
||||
homeassistant/components/screenlogic/const.py
|
||||
homeassistant/components/screenlogic/entity.py
|
||||
homeassistant/components/screenlogic/light.py
|
||||
homeassistant/components/screenlogic/number.py
|
||||
homeassistant/components/screenlogic/sensor.py
|
||||
homeassistant/components/screenlogic/services.py
|
||||
homeassistant/components/screenlogic/switch.py
|
||||
homeassistant/components/scsgate/*
|
||||
homeassistant/components/sendgrid/notify.py
|
||||
@@ -1243,6 +1224,7 @@ omit =
|
||||
homeassistant/components/smappee/switch.py
|
||||
homeassistant/components/smarty/*
|
||||
homeassistant/components/sms/__init__.py
|
||||
homeassistant/components/sms/const.py
|
||||
homeassistant/components/sms/coordinator.py
|
||||
homeassistant/components/sms/gateway.py
|
||||
homeassistant/components/sms/notify.py
|
||||
@@ -1259,8 +1241,8 @@ omit =
|
||||
homeassistant/components/solaredge/coordinator.py
|
||||
homeassistant/components/solaredge_local/sensor.py
|
||||
homeassistant/components/solarlog/__init__.py
|
||||
homeassistant/components/solarlog/coordinator.py
|
||||
homeassistant/components/solarlog/sensor.py
|
||||
homeassistant/components/solarlog/coordinator.py
|
||||
homeassistant/components/solax/__init__.py
|
||||
homeassistant/components/solax/sensor.py
|
||||
homeassistant/components/soma/__init__.py
|
||||
@@ -1297,6 +1279,13 @@ omit =
|
||||
homeassistant/components/squeezebox/__init__.py
|
||||
homeassistant/components/squeezebox/browse_media.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/account.py
|
||||
homeassistant/components/starline/binary_sensor.py
|
||||
@@ -1307,14 +1296,6 @@ omit =
|
||||
homeassistant/components/starline/sensor.py
|
||||
homeassistant/components/starline/switch.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/stiebel_eltron/*
|
||||
homeassistant/components/stookalert/__init__.py
|
||||
@@ -1336,7 +1317,6 @@ omit =
|
||||
homeassistant/components/supla/*
|
||||
homeassistant/components/surepetcare/__init__.py
|
||||
homeassistant/components/surepetcare/binary_sensor.py
|
||||
homeassistant/components/surepetcare/coordinator.py
|
||||
homeassistant/components/surepetcare/entity.py
|
||||
homeassistant/components/surepetcare/sensor.py
|
||||
homeassistant/components/swiss_hydrological_data/sensor.py
|
||||
@@ -1359,13 +1339,12 @@ omit =
|
||||
homeassistant/components/switchbot/entity.py
|
||||
homeassistant/components/switchbot/humidifier.py
|
||||
homeassistant/components/switchbot/light.py
|
||||
homeassistant/components/switchbot/lock.py
|
||||
homeassistant/components/switchbot/sensor.py
|
||||
homeassistant/components/switchbot/switch.py
|
||||
homeassistant/components/switchbot/lock.py
|
||||
homeassistant/components/switchbot_cloud/climate.py
|
||||
homeassistant/components/switchbot_cloud/coordinator.py
|
||||
homeassistant/components/switchbot_cloud/entity.py
|
||||
homeassistant/components/switchbot_cloud/sensor.py
|
||||
homeassistant/components/switchbot_cloud/switch.py
|
||||
homeassistant/components/switchmate/switch.py
|
||||
homeassistant/components/syncthing/__init__.py
|
||||
@@ -1402,6 +1381,11 @@ omit =
|
||||
homeassistant/components/tado/water_heater.py
|
||||
homeassistant/components/tami4/button.py
|
||||
homeassistant/components/tank_utility/sensor.py
|
||||
homeassistant/components/tankerkoenig/__init__.py
|
||||
homeassistant/components/tankerkoenig/binary_sensor.py
|
||||
homeassistant/components/tankerkoenig/coordinator.py
|
||||
homeassistant/components/tankerkoenig/entity.py
|
||||
homeassistant/components/tankerkoenig/sensor.py
|
||||
homeassistant/components/tapsaff/binary_sensor.py
|
||||
homeassistant/components/tautulli/__init__.py
|
||||
homeassistant/components/tautulli/coordinator.py
|
||||
@@ -1424,11 +1408,12 @@ omit =
|
||||
homeassistant/components/tensorflow/image_processing.py
|
||||
homeassistant/components/tfiac/climate.py
|
||||
homeassistant/components/thermoworks_smoke/sensor.py
|
||||
homeassistant/components/thethingsnetwork/*
|
||||
homeassistant/components/thingspeak/*
|
||||
homeassistant/components/thinkingcleaner/*
|
||||
homeassistant/components/thomson/device_tracker.py
|
||||
homeassistant/components/tibber/__init__.py
|
||||
homeassistant/components/tibber/coordinator.py
|
||||
homeassistant/components/tibber/notify.py
|
||||
homeassistant/components/tibber/sensor.py
|
||||
homeassistant/components/tikteck/light.py
|
||||
homeassistant/components/tile/__init__.py
|
||||
@@ -1445,7 +1430,6 @@ omit =
|
||||
homeassistant/components/tolo/number.py
|
||||
homeassistant/components/tolo/select.py
|
||||
homeassistant/components/tolo/sensor.py
|
||||
homeassistant/components/tolo/switch.py
|
||||
homeassistant/components/toon/__init__.py
|
||||
homeassistant/components/toon/binary_sensor.py
|
||||
homeassistant/components/toon/climate.py
|
||||
@@ -1469,7 +1453,6 @@ omit =
|
||||
homeassistant/components/traccar_server/device_tracker.py
|
||||
homeassistant/components/traccar_server/entity.py
|
||||
homeassistant/components/traccar_server/helpers.py
|
||||
homeassistant/components/traccar_server/sensor.py
|
||||
homeassistant/components/tractive/__init__.py
|
||||
homeassistant/components/tractive/binary_sensor.py
|
||||
homeassistant/components/tractive/device_tracker.py
|
||||
@@ -1517,9 +1500,9 @@ omit =
|
||||
homeassistant/components/ue_smart_radio/media_player.py
|
||||
homeassistant/components/ukraine_alarm/__init__.py
|
||||
homeassistant/components/ukraine_alarm/binary_sensor.py
|
||||
homeassistant/components/unifiled/*
|
||||
homeassistant/components/unifi_direct/__init__.py
|
||||
homeassistant/components/unifi_direct/device_tracker.py
|
||||
homeassistant/components/unifiled/*
|
||||
homeassistant/components/upb/__init__.py
|
||||
homeassistant/components/upb/light.py
|
||||
homeassistant/components/upc_connect/*
|
||||
@@ -1529,15 +1512,14 @@ omit =
|
||||
homeassistant/components/upnp/__init__.py
|
||||
homeassistant/components/upnp/device.py
|
||||
homeassistant/components/upnp/sensor.py
|
||||
homeassistant/components/vasttrafik/sensor.py
|
||||
homeassistant/components/v2c/__init__.py
|
||||
homeassistant/components/v2c/binary_sensor.py
|
||||
homeassistant/components/v2c/coordinator.py
|
||||
homeassistant/components/v2c/entity.py
|
||||
homeassistant/components/v2c/number.py
|
||||
homeassistant/components/v2c/sensor.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/binary_sensor.py
|
||||
homeassistant/components/velbus/button.py
|
||||
@@ -1545,14 +1527,15 @@ omit =
|
||||
homeassistant/components/velbus/cover.py
|
||||
homeassistant/components/velbus/entity.py
|
||||
homeassistant/components/velbus/light.py
|
||||
homeassistant/components/velbus/select.py
|
||||
homeassistant/components/velbus/sensor.py
|
||||
homeassistant/components/velbus/select.py
|
||||
homeassistant/components/velbus/switch.py
|
||||
homeassistant/components/velux/__init__.py
|
||||
homeassistant/components/velux/cover.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/coordinator.py
|
||||
homeassistant/components/venstar/sensor.py
|
||||
homeassistant/components/verisure/__init__.py
|
||||
homeassistant/components/verisure/alarm_control_panel.py
|
||||
@@ -1585,6 +1568,7 @@ omit =
|
||||
homeassistant/components/vlc_telnet/media_player.py
|
||||
homeassistant/components/vodafone_station/__init__.py
|
||||
homeassistant/components/vodafone_station/button.py
|
||||
homeassistant/components/vodafone_station/const.py
|
||||
homeassistant/components/vodafone_station/coordinator.py
|
||||
homeassistant/components/vodafone_station/device_tracker.py
|
||||
homeassistant/components/vodafone_station/sensor.py
|
||||
@@ -1609,8 +1593,10 @@ omit =
|
||||
homeassistant/components/watttime/__init__.py
|
||||
homeassistant/components/watttime/sensor.py
|
||||
homeassistant/components/weatherflow/__init__.py
|
||||
homeassistant/components/weatherflow/const.py
|
||||
homeassistant/components/weatherflow/sensor.py
|
||||
homeassistant/components/weatherflow_cloud/__init__.py
|
||||
homeassistant/components/weatherflow_cloud/const.py
|
||||
homeassistant/components/weatherflow_cloud/coordinator.py
|
||||
homeassistant/components/weatherflow_cloud/weather.py
|
||||
homeassistant/components/wiffi/__init__.py
|
||||
@@ -1628,7 +1614,6 @@ omit =
|
||||
homeassistant/components/xbox/base_sensor.py
|
||||
homeassistant/components/xbox/binary_sensor.py
|
||||
homeassistant/components/xbox/browse_media.py
|
||||
homeassistant/components/xbox/coordinator.py
|
||||
homeassistant/components/xbox/media_player.py
|
||||
homeassistant/components/xbox/remote.py
|
||||
homeassistant/components/xbox/sensor.py
|
||||
@@ -1661,7 +1646,10 @@ omit =
|
||||
homeassistant/components/xs1/*
|
||||
homeassistant/components/yale_smart_alarm/__init__.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/lock.py
|
||||
homeassistant/components/yalexs_ble/__init__.py
|
||||
homeassistant/components/yalexs_ble/binary_sensor.py
|
||||
homeassistant/components/yalexs_ble/entity.py
|
||||
@@ -1693,7 +1681,6 @@ omit =
|
||||
homeassistant/components/yolink/services.py
|
||||
homeassistant/components/yolink/siren.py
|
||||
homeassistant/components/yolink/switch.py
|
||||
homeassistant/components/yolink/valve.py
|
||||
homeassistant/components/youless/__init__.py
|
||||
homeassistant/components/youless/sensor.py
|
||||
homeassistant/components/zabbix/*
|
||||
@@ -1702,12 +1689,16 @@ omit =
|
||||
homeassistant/components/zeroconf/models.py
|
||||
homeassistant/components/zeroconf/usage.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/device.py
|
||||
homeassistant/components/zha/core/gateway.py
|
||||
homeassistant/components/zha/core/helpers.py
|
||||
homeassistant/components/zha/light.py
|
||||
homeassistant/components/zha/websocket_api.py
|
||||
homeassistant/components/zhong_hong/climate.py
|
||||
homeassistant/components/ziggo_mediabox_xl/media_player.py
|
||||
homeassistant/components/zoneminder/*
|
||||
@@ -1724,6 +1715,15 @@ omit =
|
||||
homeassistant/components/zwave_me/sensor.py
|
||||
homeassistant/components/zwave_me/siren.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]
|
||||
|
@@ -4,12 +4,7 @@
|
||||
"dockerFile": "../Dockerfile.dev",
|
||||
"postCreateCommand": "script/setup",
|
||||
"postStartCommand": "script/bootstrap",
|
||||
"containerEnv": {
|
||||
"PYTHONASYNCIODEBUG": "1"
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
"containerEnv": { "DEVCONTAINER": "1" },
|
||||
// Port 5683 udp is used by Shelly integration
|
||||
"appPort": ["8123:8123", "5683:5683/udp"],
|
||||
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
|
||||
@@ -22,15 +17,11 @@
|
||||
"visualstudioexptteam.vscodeintellicode",
|
||||
"redhat.vscode-yaml",
|
||||
"esbenp.prettier-vscode",
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"GitHub.copilot"
|
||||
"GitHub.vscode-pull-request-github"
|
||||
],
|
||||
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
|
||||
"settings": {
|
||||
"python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
|
||||
"python.pythonPath": "/home/vscode/.local/ha-venv/bin/python",
|
||||
"python.terminal.activateEnvInCurrentTerminal": true,
|
||||
"python.pythonPath": "/usr/local/bin/python",
|
||||
"python.testing.pytestArgs": ["--no-cov"],
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
|
@@ -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
|
235
.github/workflows/builder.yml
vendored
235
.github/workflows/builder.yml
vendored
@@ -12,8 +12,6 @@ env:
|
||||
BUILD_TYPE: core
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
PIP_TIMEOUT: 60
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
|
||||
jobs:
|
||||
init:
|
||||
@@ -27,12 +25,12 @@ jobs:
|
||||
publish: ${{ steps.version.outputs.publish }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -51,29 +49,41 @@ jobs:
|
||||
with:
|
||||
ignore-dev: true
|
||||
|
||||
- name: Fail if translations files are checked in
|
||||
run: |
|
||||
if [ -n "$(find homeassistant/components/*/translations -type f)" ]; then
|
||||
echo "Translations files are checked in, please remove the following files:"
|
||||
find homeassistant/components/*/translations -type f
|
||||
exit 1
|
||||
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.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download Translations
|
||||
run: python3 -m script.translations download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
|
||||
- name: Archive translations
|
||||
- name: Build package
|
||||
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
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
if-no-files-found: error
|
||||
- name: Upload package
|
||||
shell: bash
|
||||
run: |
|
||||
export TWINE_USERNAME="__token__"
|
||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||
|
||||
twine upload dist/* --skip-existing
|
||||
|
||||
build_base:
|
||||
name: Build ${{ matrix.arch }} base core image
|
||||
@@ -85,16 +95,15 @@ jobs:
|
||||
packages: write
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.2
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v3.1.4
|
||||
uses: dawidd6/action-download-artifact@v3.1.2
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -105,7 +114,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v3.1.4
|
||||
uses: dawidd6/action-download-artifact@v3.1.2
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/intents-package
|
||||
@@ -116,20 +125,17 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Adjust nightly version
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
shell: bash
|
||||
env:
|
||||
UV_PRERELEASE: allow
|
||||
run: |
|
||||
python3 -m pip install "$(grep '^uv' < requirements_test.txt)"
|
||||
uv pip install packaging tomli
|
||||
uv pip install .
|
||||
python3 script/version_bump.py nightly --set-nightly-version "${{ needs.init.outputs.version }}"
|
||||
python3 -m pip install packaging tomli
|
||||
python3 -m pip install .
|
||||
version="$(python3 script/version_bump.py nightly)"
|
||||
|
||||
if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then
|
||||
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]}|" \
|
||||
homeassistant/package_constraints.txt
|
||||
|
||||
sed -i "s|home-assistant-frontend==.*||" requirements_all.txt
|
||||
python -m script.gen_requirements_all
|
||||
fi
|
||||
|
||||
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]}|" \
|
||||
homeassistant/package_constraints.txt
|
||||
|
||||
sed -i "s|home-assistant-intents==.*||" requirements_all.txt
|
||||
python -m script.gen_requirements_all
|
||||
fi
|
||||
|
||||
- name: Adjustments for armhf
|
||||
@@ -174,15 +180,19 @@ jobs:
|
||||
sed -i "s|pyezviz|# pyezviz|g" requirements_all.txt
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
name: translations
|
||||
|
||||
- name: Extract translations
|
||||
- name: Adjustments for 64-bit
|
||||
if: matrix.arch == 'amd64' || matrix.arch == 'aarch64'
|
||||
run: |
|
||||
tar xvf translations.tar.gz
|
||||
rm translations.tar.gz
|
||||
# Some speedups are only available on 64-bit, and since
|
||||
# we build 32bit images on 64bit hosts, we only enable
|
||||
# the speed ups on 64bit since the wheels for 32bit
|
||||
# are not available.
|
||||
sed -i "s|aiohttp-zlib-ng|aiohttp-zlib-ng\[isal\]|g" requirements_all.txt
|
||||
|
||||
- name: Download Translations
|
||||
run: python3 -m script.translations download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
|
||||
- name: Write meta info file
|
||||
shell: bash
|
||||
@@ -190,14 +200,14 @@ jobs:
|
||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.2.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2024.03.5
|
||||
uses: home-assistant/builder@2024.01.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -206,6 +216,17 @@ jobs:
|
||||
--target /data \
|
||||
--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:
|
||||
name: Build ${{ matrix.machine }} machine core image
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
@@ -242,7 +263,7 @@ jobs:
|
||||
- green
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.2
|
||||
|
||||
- name: Set build additional args
|
||||
run: |
|
||||
@@ -256,14 +277,14 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.2.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2024.03.5
|
||||
uses: home-assistant/builder@2024.01.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -279,7 +300,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.2
|
||||
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master
|
||||
@@ -315,28 +336,23 @@ jobs:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.2
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.5.0
|
||||
uses: sigstore/cosign-installer@v3.4.0
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
cosign-release: "v2.0.2"
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@v3.2.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||
uses: docker/login-action@v3.2.0
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -350,37 +366,41 @@ jobs:
|
||||
function create_manifest() {
|
||||
local tag_l=${1}
|
||||
local tag_r=${2}
|
||||
local registry=${{ matrix.registry }}
|
||||
|
||||
docker manifest create "${registry}/home-assistant:${tag_l}" \
|
||||
"${registry}/amd64-homeassistant:${tag_r}" \
|
||||
"${registry}/i386-homeassistant:${tag_r}" \
|
||||
"${registry}/armhf-homeassistant:${tag_r}" \
|
||||
"${registry}/armv7-homeassistant:${tag_r}" \
|
||||
"${registry}/aarch64-homeassistant:${tag_r}"
|
||||
for registry in "ghcr.io/home-assistant" "docker.io/homeassistant"
|
||||
do
|
||||
|
||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
||||
"${registry}/amd64-homeassistant:${tag_r}" \
|
||||
--os linux --arch amd64
|
||||
docker manifest create "${registry}/home-assistant:${tag_l}" \
|
||||
"${registry}/amd64-homeassistant:${tag_r}" \
|
||||
"${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}" \
|
||||
"${registry}/i386-homeassistant:${tag_r}" \
|
||||
--os linux --arch 386
|
||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
||||
"${registry}/amd64-homeassistant:${tag_r}" \
|
||||
--os linux --arch amd64
|
||||
|
||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
||||
"${registry}/armhf-homeassistant:${tag_r}" \
|
||||
--os linux --arch arm --variant=v6
|
||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
||||
"${registry}/i386-homeassistant:${tag_r}" \
|
||||
--os linux --arch 386
|
||||
|
||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
||||
"${registry}/armv7-homeassistant:${tag_r}" \
|
||||
--os linux --arch arm --variant=v7
|
||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
||||
"${registry}/armhf-homeassistant:${tag_r}" \
|
||||
--os linux --arch arm --variant=v6
|
||||
|
||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
||||
"${registry}/aarch64-homeassistant:${tag_r}" \
|
||||
--os linux --arch arm64 --variant=v8
|
||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
||||
"${registry}/armv7-homeassistant:${tag_r}" \
|
||||
--os linux --arch arm --variant=v7
|
||||
|
||||
docker manifest push --purge "${registry}/home-assistant:${tag_l}"
|
||||
cosign sign --yes "${registry}/home-assistant:${tag_l}"
|
||||
docker manifest annotate "${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() {
|
||||
@@ -413,14 +433,12 @@ jobs:
|
||||
validate_image "ghcr.io/home-assistant/armv7-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
|
||||
push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}"
|
||||
push_dockerhub "i386-homeassistant" "${{ needs.init.outputs.version }}"
|
||||
push_dockerhub "armhf-homeassistant" "${{ needs.init.outputs.version }}"
|
||||
push_dockerhub "armv7-homeassistant" "${{ needs.init.outputs.version }}"
|
||||
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
|
||||
fi
|
||||
# Upload images to dockerhub
|
||||
push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}"
|
||||
push_dockerhub "i386-homeassistant" "${{ needs.init.outputs.version }}"
|
||||
push_dockerhub "armhf-homeassistant" "${{ needs.init.outputs.version }}"
|
||||
push_dockerhub "armv7-homeassistant" "${{ needs.init.outputs.version }}"
|
||||
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
|
||||
|
||||
# Create version tag
|
||||
create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}"
|
||||
@@ -441,44 +459,3 @@ jobs:
|
||||
v="${{ needs.init.outputs.version }}"
|
||||
create_manifest "${v%.*}" "${{ needs.init.outputs.version }}"
|
||||
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
|
||||
|
415
.github/workflows/ci.yaml
vendored
415
.github/workflows/ci.yaml
vendored
@@ -33,10 +33,10 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 8
|
||||
CACHE_VERSION: 5
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 8
|
||||
HA_SHORT_VERSION: "2024.6"
|
||||
HA_SHORT_VERSION: "2024.4"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -89,18 +89,14 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- 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
|
||||
id: generate_python_cache_key
|
||||
run: |
|
||||
# Include HA_SHORT_VERSION to force the immediate creation
|
||||
# of a new uv cache entry after a version bump.
|
||||
echo "key=venv-${{ env.CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}-${{
|
||||
hashFiles('requirements_test.txt', 'requirements_test_pre_commit.txt') }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
run: >-
|
||||
echo "key=venv-${{ env.CACHE_VERSION }}-${{
|
||||
hashFiles('requirements_test.txt') }}-${{
|
||||
hashFiles('requirements_all.txt') }}-${{
|
||||
hashFiles('homeassistant/package_constraints.txt') }}-${{
|
||||
hashFiles('script/gen_requirements_all.py') }}" >> $GITHUB_OUTPUT
|
||||
hashFiles('homeassistant/package_constraints.txt') }}" >> $GITHUB_OUTPUT
|
||||
- name: Generate partial pre-commit restore key
|
||||
id: generate_pre-commit_cache_key
|
||||
run: >-
|
||||
@@ -226,16 +222,16 @@ jobs:
|
||||
- info
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.0.1
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -247,11 +243,11 @@ jobs:
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
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)"
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.0.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
@@ -272,16 +268,16 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- 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 }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.0.2
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -290,7 +286,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.0.2
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -312,16 +308,16 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- 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 }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.0.2
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -330,7 +326,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.0.2
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -351,16 +347,16 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- 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 }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.0.2
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -369,7 +365,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@v4.0.2
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -445,23 +441,21 @@ jobs:
|
||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Generate partial uv restore key
|
||||
id: generate-uv-key
|
||||
run: |
|
||||
uv_version=$(cat requirements_test.txt | grep uv | cut -d '=' -f 3)
|
||||
echo "version=${uv_version}" >> $GITHUB_OUTPUT
|
||||
echo "key=uv-${{ env.UV_CACHE_VERSION }}-${uv_version}-${{
|
||||
run: >-
|
||||
echo "key=uv-${{ env.UV_CACHE_VERSION }}-${{
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.0.1
|
||||
with:
|
||||
path: venv
|
||||
lookup-only: true
|
||||
@@ -470,20 +464,17 @@ jobs:
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.0.1
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
path: ${{ env.UV_CACHE }}
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
steps.generate-uv-key.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{
|
||||
env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{
|
||||
env.HA_SHORT_VERSION }}-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{ env.UV_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}-
|
||||
- name: Install additional OS dependencies
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install \
|
||||
bluez \
|
||||
@@ -493,7 +484,6 @@ jobs:
|
||||
libavfilter-dev \
|
||||
libavformat-dev \
|
||||
libavutil-dev \
|
||||
libgammu-dev \
|
||||
libswresample-dev \
|
||||
libswscale-dev \
|
||||
libudev-dev
|
||||
@@ -503,11 +493,9 @@ jobs:
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
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 -r requirements.txt
|
||||
python -m script.gen_requirements_all ci
|
||||
uv pip install -r requirements_all_pytest.txt
|
||||
uv pip install -r requirements_all.txt
|
||||
uv pip install -r requirements_test.txt
|
||||
uv pip install -e . --config-settings editable_mode=compat
|
||||
|
||||
@@ -522,16 +510,16 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.0.2
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -554,16 +542,16 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.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
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -587,16 +575,16 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.0.2
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -611,14 +599,14 @@ jobs:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pylint --ignore-missing-annotations=y homeassistant
|
||||
pylint --ignore-missing-annotations=y --ignore-wrong-coordinator-module=y homeassistant
|
||||
- name: Run pylint (partially)
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pylint --ignore-missing-annotations=y 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:
|
||||
name: Check mypy
|
||||
@@ -631,10 +619,10 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -647,7 +635,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.0.2
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -655,7 +643,7 @@ jobs:
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.0.1
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
@@ -682,63 +670,14 @@ jobs:
|
||||
python --version
|
||||
mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
||||
|
||||
prepare-pytest-full:
|
||||
pytest:
|
||||
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:
|
||||
- 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.info.outputs.test_full_suite == 'true' || needs.info.outputs.tests_glob)
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
@@ -747,7 +686,6 @@ jobs:
|
||||
- lint-other
|
||||
- lint-ruff
|
||||
- mypy
|
||||
- prepare-pytest-full
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -758,23 +696,21 @@ jobs:
|
||||
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
|
||||
ffmpeg
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.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
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -786,15 +722,12 @@ jobs:
|
||||
- name: Register pytest slow test problem matcher
|
||||
run: |
|
||||
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
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python3 -m script.translations develop --all
|
||||
- name: Run pytest
|
||||
- name: Run pytest (fully)
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
timeout-minutes: 60
|
||||
id: pytest-full
|
||||
env:
|
||||
@@ -815,27 +748,62 @@ jobs:
|
||||
--durations=10 \
|
||||
-n auto \
|
||||
--dist=loadfile \
|
||||
--test-group-count ${{ needs.info.outputs.test_group_count }} \
|
||||
--test-group=${{ matrix.group }} \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
-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
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
if: success() || failure() && (steps.pytest-full.conclusion == 'failure' || steps.pytest-partial.conclusion == 'failure')
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
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
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Remove pytest_buckets
|
||||
run: rm pytest_buckets.txt
|
||||
- name: Check dirty
|
||||
run: |
|
||||
./script/check_dirty
|
||||
@@ -874,23 +842,22 @@ jobs:
|
||||
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 \
|
||||
libmariadb-dev-compat
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.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
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -945,7 +912,7 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -953,7 +920,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
@@ -997,23 +964,22 @@ jobs:
|
||||
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 \
|
||||
postgresql-server-dev-14
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.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
|
||||
uses: actions/cache/restore@v4.0.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1069,7 +1035,7 @@ jobs:
|
||||
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
|
||||
- name: Upload pytest output
|
||||
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1077,7 +1043,7 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Upload coverage artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-${{
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
@@ -1087,160 +1053,39 @@ jobs:
|
||||
run: |
|
||||
./script/check_dirty
|
||||
|
||||
coverage-full:
|
||||
name: Upload test coverage to Codecov (full suite)
|
||||
coverage:
|
||||
name: Upload test coverage to Codecov
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- info
|
||||
- pytest-full
|
||||
- pytest-postgres
|
||||
- pytest-mariadb
|
||||
- pytest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
- name: Upload coverage to Codecov (full coverage)
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
uses: codecov/codecov-action@v4.4.1
|
||||
uses: Wandalen/wretry.action@v1.4.10
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
pytest-partial:
|
||||
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.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
|
||||
action: codecov/codecov-action@v3.1.3
|
||||
with: |
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
token: ${{ env.CODECOV_TOKEN }}
|
||||
attempt_limit: 5
|
||||
attempt_delay: 30000
|
||||
- name: Upload coverage to Codecov (partial coverage)
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
uses: codecov/codecov-action@v4.4.1
|
||||
uses: Wandalen/wretry.action@v1.4.10
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
action: codecov/codecov-action@v3.1.3
|
||||
with: |
|
||||
fail_ci_if_error: true
|
||||
token: ${{ env.CODECOV_TOKEN }}
|
||||
attempt_limit: 5
|
||||
attempt_delay: 30000
|
||||
|
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.25.6
|
||||
uses: github/codeql-action/init@v3.24.7
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.25.6
|
||||
uses: github/codeql-action/analyze@v3.24.7
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@@ -19,10 +19,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.0.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
95
.github/workflows/wheels.yml
vendored
95
.github/workflows/wheels.yml
vendored
@@ -14,10 +14,6 @@ on:
|
||||
- "homeassistant/package_constraints.txt"
|
||||
- "requirements_all.txt"
|
||||
- "requirements.txt"
|
||||
- "script/gen_requirements_all.py"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name}}
|
||||
@@ -32,22 +28,7 @@ jobs:
|
||||
architectures: ${{ steps.info.outputs.architectures }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
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: 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
|
||||
uses: actions/checkout@v4.1.2
|
||||
|
||||
- name: Get information
|
||||
id: info
|
||||
@@ -82,30 +63,19 @@ jobs:
|
||||
) > .env_file
|
||||
|
||||
- name: Upload env_file
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
overwrite: true
|
||||
|
||||
- name: Upload requirements_diff
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
with:
|
||||
name: requirements_diff
|
||||
path: ./requirements_diff.txt
|
||||
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:
|
||||
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
@@ -118,15 +88,15 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@@ -156,30 +126,57 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
uses: actions/download-artifact@v4.1.4
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
- name: (Un)comment packages
|
||||
run: |
|
||||
requirement_files="requirements_all.txt requirements_diff.txt"
|
||||
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
|
||||
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
|
||||
# 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
|
||||
run: |
|
||||
@@ -211,7 +208,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_old-cython.txt"
|
||||
@@ -226,7 +223,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtaa"
|
||||
@@ -240,7 +237,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtab"
|
||||
@@ -254,7 +251,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtac"
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -34,7 +34,6 @@ Icon
|
||||
|
||||
# GITHUB Proposed Python stuff:
|
||||
*.py[cod]
|
||||
__pycache__
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
@@ -133,6 +132,3 @@ tmp_cache
|
||||
|
||||
# python-language-server / Rope
|
||||
.ropeproject
|
||||
|
||||
# Will be created from script/split_tests.py
|
||||
pytest_buckets.txt
|
@@ -1,21 +1,21 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.4.6
|
||||
rev: v0.2.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
- --fix
|
||||
- id: ruff-format
|
||||
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$
|
||||
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.py$
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
rev: v2.2.2
|
||||
hooks:
|
||||
- id: codespell
|
||||
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"
|
||||
- --quiet-level=2
|
||||
exclude_types: [csv, json, html]
|
||||
exclude_types: [csv, json]
|
||||
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
@@ -30,7 +30,7 @@ repos:
|
||||
- --branch=master
|
||||
- --branch=rc
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.35.1
|
||||
rev: v1.32.0
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
@@ -61,15 +61,15 @@ repos:
|
||||
name: mypy
|
||||
entry: script/run-in-env.sh mypy
|
||||
language: script
|
||||
types_or: [python, pyi]
|
||||
types: [python]
|
||||
require_serial: true
|
||||
files: ^(homeassistant|pylint)/.+\.(py|pyi)$
|
||||
files: ^(homeassistant|pylint)/.+\.py$
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y
|
||||
language: script
|
||||
types_or: [python, pyi]
|
||||
files: ^homeassistant/.+\.(py|pyi)$
|
||||
types: [python]
|
||||
files: ^homeassistant/.+\.py$
|
||||
- id: gen_requirements_all
|
||||
name: gen_requirements_all
|
||||
entry: script/run-in-env.sh python3 -m script.gen_requirements_all
|
||||
@@ -83,7 +83,7 @@ repos:
|
||||
pass_filenames: false
|
||||
language: script
|
||||
types: [text]
|
||||
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|\.coveragerc|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|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
|
||||
name: hassfest-metadata
|
||||
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
|
||||
|
@@ -48,7 +48,6 @@ homeassistant.components.adax.*
|
||||
homeassistant.components.adguard.*
|
||||
homeassistant.components.aftership.*
|
||||
homeassistant.components.air_quality.*
|
||||
homeassistant.components.airgradient.*
|
||||
homeassistant.components.airly.*
|
||||
homeassistant.components.airnow.*
|
||||
homeassistant.components.airq.*
|
||||
@@ -66,7 +65,7 @@ homeassistant.components.alexa.*
|
||||
homeassistant.components.alpha_vantage.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.amberelectric.*
|
||||
homeassistant.components.ambient_network.*
|
||||
homeassistant.components.ambiclimate.*
|
||||
homeassistant.components.ambient_station.*
|
||||
homeassistant.components.amcrest.*
|
||||
homeassistant.components.ampio.*
|
||||
@@ -84,7 +83,6 @@ homeassistant.components.api.*
|
||||
homeassistant.components.apple_tv.*
|
||||
homeassistant.components.apprise.*
|
||||
homeassistant.components.aprs.*
|
||||
homeassistant.components.apsystems.*
|
||||
homeassistant.components.aqualogic.*
|
||||
homeassistant.components.aquostv.*
|
||||
homeassistant.components.aranet.*
|
||||
@@ -168,12 +166,10 @@ homeassistant.components.electric_kiwi.*
|
||||
homeassistant.components.elgato.*
|
||||
homeassistant.components.elkm1.*
|
||||
homeassistant.components.emulated_hue.*
|
||||
homeassistant.components.energenie_power_sockets.*
|
||||
homeassistant.components.energy.*
|
||||
homeassistant.components.energyzero.*
|
||||
homeassistant.components.enigma2.*
|
||||
homeassistant.components.enphase_envoy.*
|
||||
homeassistant.components.eq3btsmart.*
|
||||
homeassistant.components.esphome.*
|
||||
homeassistant.components.event.*
|
||||
homeassistant.components.evil_genius_labs.*
|
||||
@@ -236,7 +232,6 @@ homeassistant.components.homeworks.*
|
||||
homeassistant.components.http.*
|
||||
homeassistant.components.huawei_lte.*
|
||||
homeassistant.components.humidifier.*
|
||||
homeassistant.components.husqvarna_automower.*
|
||||
homeassistant.components.hydrawise.*
|
||||
homeassistant.components.hyperion.*
|
||||
homeassistant.components.ibeacon.*
|
||||
@@ -245,7 +240,6 @@ homeassistant.components.image.*
|
||||
homeassistant.components.image_processing.*
|
||||
homeassistant.components.image_upload.*
|
||||
homeassistant.components.imap.*
|
||||
homeassistant.components.imgw_pib.*
|
||||
homeassistant.components.input_button.*
|
||||
homeassistant.components.input_select.*
|
||||
homeassistant.components.input_text.*
|
||||
@@ -302,7 +296,6 @@ homeassistant.components.minecraft_server.*
|
||||
homeassistant.components.mjpeg.*
|
||||
homeassistant.components.modbus.*
|
||||
homeassistant.components.modem_callerid.*
|
||||
homeassistant.components.monzo.*
|
||||
homeassistant.components.moon.*
|
||||
homeassistant.components.mopeka.*
|
||||
homeassistant.components.motionmount.*
|
||||
@@ -341,6 +334,7 @@ homeassistant.components.persistent_notification.*
|
||||
homeassistant.components.pi_hole.*
|
||||
homeassistant.components.ping.*
|
||||
homeassistant.components.plugwise.*
|
||||
homeassistant.components.poolsense.*
|
||||
homeassistant.components.powerwall.*
|
||||
homeassistant.components.private_ble_device.*
|
||||
homeassistant.components.prometheus.*
|
||||
@@ -367,7 +361,6 @@ homeassistant.components.rest_command.*
|
||||
homeassistant.components.rfxtrx.*
|
||||
homeassistant.components.rhasspy.*
|
||||
homeassistant.components.ridwell.*
|
||||
homeassistant.components.ring.*
|
||||
homeassistant.components.rituals_perfume_genie.*
|
||||
homeassistant.components.roku.*
|
||||
homeassistant.components.romy.*
|
||||
@@ -428,7 +421,6 @@ homeassistant.components.tcp.*
|
||||
homeassistant.components.technove.*
|
||||
homeassistant.components.tedee.*
|
||||
homeassistant.components.text.*
|
||||
homeassistant.components.thethingsnetwork.*
|
||||
homeassistant.components.threshold.*
|
||||
homeassistant.components.tibber.*
|
||||
homeassistant.components.tile.*
|
||||
|
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
@@ -103,7 +103,7 @@
|
||||
{
|
||||
"label": "Install all Requirements",
|
||||
"type": "shell",
|
||||
"command": "uv pip install -r requirements_all.txt",
|
||||
"command": "pip3 install -r requirements_all.txt",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
@@ -117,7 +117,7 @@
|
||||
{
|
||||
"label": "Install all Test Requirements",
|
||||
"type": "shell",
|
||||
"command": "uv pip install -r requirements_test_all.txt",
|
||||
"command": "pip3 install -r requirements_test_all.txt",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
140
CODEOWNERS
140
CODEOWNERS
@@ -5,30 +5,13 @@
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# Home Assistant Core
|
||||
.core_files.yaml @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
|
||||
setup.cfg @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/auth/ @home-assistant/core
|
||||
/homeassistant/backports/ @home-assistant/core
|
||||
/homeassistant/helpers/ @home-assistant/core
|
||||
/homeassistant/scripts/ @home-assistant/core
|
||||
/homeassistant/util/ @home-assistant/core
|
||||
/pylint/ @home-assistant/core
|
||||
/script/ @home-assistant/core
|
||||
|
||||
# Home Assistant Supervisor
|
||||
.dockerignore @home-assistant/supervisor
|
||||
build.json @home-assistant/supervisor
|
||||
/machine/ @home-assistant/supervisor
|
||||
/rootfs/ @home-assistant/supervisor
|
||||
@@ -56,8 +39,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/agent_dvr/ @ispysoftware
|
||||
/homeassistant/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
|
||||
/tests/components/airly/ @bieniu
|
||||
/homeassistant/components/airnow/ @asymworks
|
||||
@@ -80,8 +61,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/airzone/ @Noltari
|
||||
/homeassistant/components/airzone_cloud/ @Noltari
|
||||
/tests/components/airzone_cloud/ @Noltari
|
||||
/homeassistant/components/aladdin_connect/ @swcloudgenie
|
||||
/tests/components/aladdin_connect/ @swcloudgenie
|
||||
/homeassistant/components/aladdin_connect/ @mkmer
|
||||
/tests/components/aladdin_connect/ @mkmer
|
||||
/homeassistant/components/alarm_control_panel/ @home-assistant/core
|
||||
/tests/components/alarm_control_panel/ @home-assistant/core
|
||||
/homeassistant/components/alert/ @home-assistant/core @frenck
|
||||
@@ -90,8 +71,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/homeassistant/components/amberelectric/ @madpilot
|
||||
/tests/components/amberelectric/ @madpilot
|
||||
/homeassistant/components/ambient_network/ @thomaskistler
|
||||
/tests/components/ambient_network/ @thomaskistler
|
||||
/homeassistant/components/ambiclimate/ @danielhiversen
|
||||
/tests/components/ambiclimate/ @danielhiversen
|
||||
/homeassistant/components/ambient_station/ @bachya
|
||||
/tests/components/ambient_station/ @bachya
|
||||
/homeassistant/components/amcrest/ @flacjacket
|
||||
@@ -127,15 +108,11 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/aprilaire/ @chamberlain2007
|
||||
/homeassistant/components/aprs/ @PhilRW
|
||||
/tests/components/aprs/ @PhilRW
|
||||
/homeassistant/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
|
||||
/tests/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
|
||||
/homeassistant/components/aranet/ @aschmitz @thecode @anrijs
|
||||
/tests/components/aranet/ @aschmitz @thecode @anrijs
|
||||
/homeassistant/components/aranet/ @aschmitz @thecode
|
||||
/tests/components/aranet/ @aschmitz @thecode
|
||||
/homeassistant/components/arcam_fmj/ @elupus
|
||||
/tests/components/arcam_fmj/ @elupus
|
||||
/homeassistant/components/arris_tg2492lg/ @vanbalken
|
||||
/homeassistant/components/arve/ @ikalnyi
|
||||
/tests/components/arve/ @ikalnyi
|
||||
/homeassistant/components/aseko_pool_live/ @milanmeu
|
||||
/tests/components/aseko_pool_live/ @milanmeu
|
||||
/homeassistant/components/assist_pipeline/ @balloob @synesthesiam
|
||||
@@ -163,8 +140,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/awair/ @ahayworth @danielsjf
|
||||
/homeassistant/components/axis/ @Kane610
|
||||
/tests/components/axis/ @Kane610
|
||||
/homeassistant/components/azure_data_explorer/ @kaareseras
|
||||
/tests/components/azure_data_explorer/ @kaareseras
|
||||
/homeassistant/components/azure_devops/ @timmo001
|
||||
/tests/components/azure_devops/ @timmo001
|
||||
/homeassistant/components/azure_event_hub/ @eavanvalkenburg
|
||||
@@ -324,8 +299,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/discovergy/ @jpbede
|
||||
/homeassistant/components/dlink/ @tkdrob
|
||||
/tests/components/dlink/ @tkdrob
|
||||
/homeassistant/components/dlna_dmr/ @chishm
|
||||
/tests/components/dlna_dmr/ @chishm
|
||||
/homeassistant/components/dlna_dmr/ @StevenLooman @chishm
|
||||
/tests/components/dlna_dmr/ @StevenLooman @chishm
|
||||
/homeassistant/components/dlna_dms/ @chishm
|
||||
/tests/components/dlna_dms/ @chishm
|
||||
/homeassistant/components/dnsip/ @gjohansson-ST
|
||||
@@ -334,16 +309,14 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/doorbird/ @oblogic7 @bdraco @flacjacket
|
||||
/homeassistant/components/dormakaba_dkey/ @emontnemery
|
||||
/tests/components/dormakaba_dkey/ @emontnemery
|
||||
/homeassistant/components/downloader/ @erwindouna
|
||||
/tests/components/downloader/ @erwindouna
|
||||
/homeassistant/components/dremel_3d_printer/ @tkdrob
|
||||
/tests/components/dremel_3d_printer/ @tkdrob
|
||||
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/homeassistant/components/dsmr/ @Robbie1221 @frenck
|
||||
/tests/components/dsmr/ @Robbie1221 @frenck
|
||||
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox
|
||||
/tests/components/dsmr_reader/ @sorted-bits @glodenox
|
||||
/homeassistant/components/duotecno/ @cereal2nd
|
||||
/tests/components/duotecno/ @cereal2nd
|
||||
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
|
||||
@@ -386,14 +359,11 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/emulated_hue/ @bdraco @Tho85
|
||||
/homeassistant/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
|
||||
/tests/components/energy/ @home-assistant/core
|
||||
/homeassistant/components/energyzero/ @klaasnicolaas
|
||||
/tests/components/energyzero/ @klaasnicolaas
|
||||
/homeassistant/components/enigma2/ @autinerd
|
||||
/tests/components/enigma2/ @autinerd
|
||||
/homeassistant/components/enocean/ @bdurrer
|
||||
/tests/components/enocean/ @bdurrer
|
||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek @catsmanac
|
||||
@@ -402,14 +372,11 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||
/homeassistant/components/ephember/ @ttroy50
|
||||
/homeassistant/components/epic_games_store/ @hacf-fr @Quentame
|
||||
/tests/components/epic_games_store/ @hacf-fr @Quentame
|
||||
/homeassistant/components/epion/ @lhgravendeel
|
||||
/tests/components/epion/ @lhgravendeel
|
||||
/homeassistant/components/epson/ @pszafer
|
||||
/tests/components/epson/ @pszafer
|
||||
/homeassistant/components/eq3btsmart/ @eulemitkeule @dbuezas
|
||||
/tests/components/eq3btsmart/ @eulemitkeule @dbuezas
|
||||
/homeassistant/components/epsonworkforce/ @ThaStealth
|
||||
/homeassistant/components/escea/ @lazdavila
|
||||
/tests/components/escea/ @lazdavila
|
||||
/homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
|
||||
@@ -466,8 +433,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/forked_daapd/ @uvjustin
|
||||
/tests/components/forked_daapd/ @uvjustin
|
||||
/homeassistant/components/fortios/ @kimfrellsen
|
||||
/homeassistant/components/foscam/ @krmarien
|
||||
/tests/components/foscam/ @krmarien
|
||||
/homeassistant/components/foscam/ @skgsergio @krmarien
|
||||
/tests/components/foscam/ @skgsergio @krmarien
|
||||
/homeassistant/components/freebox/ @hacf-fr @Quentame
|
||||
/tests/components/freebox/ @hacf-fr @Quentame
|
||||
/homeassistant/components/freedompro/ @stefano055415
|
||||
@@ -486,8 +453,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/frontier_silicon/ @wlcrs
|
||||
/homeassistant/components/fully_kiosk/ @cgarwood
|
||||
/tests/components/fully_kiosk/ @cgarwood
|
||||
/homeassistant/components/fyta/ @dontinelli
|
||||
/tests/components/fyta/ @dontinelli
|
||||
/homeassistant/components/garages_amsterdam/ @klaasnicolaas
|
||||
/tests/components/garages_amsterdam/ @klaasnicolaas
|
||||
/homeassistant/components/gardena_bluetooth/ @elupus
|
||||
@@ -554,14 +519,14 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/group/ @home-assistant/core
|
||||
/homeassistant/components/guardian/ @bachya
|
||||
/tests/components/guardian/ @bachya
|
||||
/homeassistant/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r
|
||||
/tests/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r
|
||||
/homeassistant/components/habitica/ @ASMfreaK @leikoilja
|
||||
/tests/components/habitica/ @ASMfreaK @leikoilja
|
||||
/homeassistant/components/hardkernel/ @home-assistant/core
|
||||
/tests/components/hardkernel/ @home-assistant/core
|
||||
/homeassistant/components/hardware/ @home-assistant/core
|
||||
/tests/components/hardware/ @home-assistant/core
|
||||
/homeassistant/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
|
||||
/tests/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
|
||||
/homeassistant/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan
|
||||
/tests/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan
|
||||
/homeassistant/components/hassio/ @home-assistant/supervisor
|
||||
/tests/components/hassio/ @home-assistant/supervisor
|
||||
/homeassistant/components/hdmi_cec/ @inytar
|
||||
@@ -603,10 +568,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/homekit/ @bdraco
|
||||
/homeassistant/components/homekit_controller/ @Jc2k @bdraco
|
||||
/tests/components/homekit_controller/ @Jc2k @bdraco
|
||||
/homeassistant/components/homematic/ @pvizeli
|
||||
/tests/components/homematic/ @pvizeli
|
||||
/homeassistant/components/homematicip_cloud/ @hahn-th
|
||||
/tests/components/homematicip_cloud/ @hahn-th
|
||||
/homeassistant/components/homematic/ @pvizeli @danielperna84
|
||||
/tests/components/homematic/ @pvizeli @danielperna84
|
||||
/homeassistant/components/homewizard/ @DCSBL
|
||||
/tests/components/homewizard/ @DCSBL
|
||||
/homeassistant/components/honeywell/ @rdfurman @mkmer
|
||||
@@ -654,8 +617,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/image_upload/ @home-assistant/core
|
||||
/homeassistant/components/imap/ @jbouwh
|
||||
/tests/components/imap/ @jbouwh
|
||||
/homeassistant/components/imgw_pib/ @bieniu
|
||||
/tests/components/imgw_pib/ @bieniu
|
||||
/homeassistant/components/improv_ble/ @emontnemery
|
||||
/tests/components/improv_ble/ @emontnemery
|
||||
/homeassistant/components/incomfort/ @zxdavb
|
||||
@@ -696,10 +657,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/iqvia/ @bachya
|
||||
/tests/components/iqvia/ @bachya
|
||||
/homeassistant/components/irish_rail_transport/ @ttroy50
|
||||
/homeassistant/components/isal/ @bdraco
|
||||
/tests/components/isal/ @bdraco
|
||||
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||
/homeassistant/components/islamic_prayer_times/ @engrbm87
|
||||
/tests/components/islamic_prayer_times/ @engrbm87
|
||||
/homeassistant/components/iss/ @DurgNomis-drol
|
||||
/tests/components/iss/ @DurgNomis-drol
|
||||
/homeassistant/components/isy994/ @bdraco @shbatm
|
||||
@@ -768,8 +727,7 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/leaone/ @bdraco
|
||||
/homeassistant/components/led_ble/ @bdraco
|
||||
/tests/components/led_ble/ @bdraco
|
||||
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
|
||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||
/homeassistant/components/lg_netcast/ @Drafteed
|
||||
/homeassistant/components/lidarr/ @tkdrob
|
||||
/tests/components/lidarr/ @tkdrob
|
||||
/homeassistant/components/light/ @home-assistant/core
|
||||
@@ -873,22 +831,18 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
||||
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
||||
/tests/components/monoprice/ @etsinko @OnFreund
|
||||
/homeassistant/components/monzo/ @jakemartin-icl
|
||||
/tests/components/monzo/ @jakemartin-icl
|
||||
/homeassistant/components/moon/ @fabaff @frenck
|
||||
/tests/components/moon/ @fabaff @frenck
|
||||
/homeassistant/components/mopeka/ @bdraco
|
||||
/tests/components/mopeka/ @bdraco
|
||||
/homeassistant/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
|
||||
/tests/components/motioneye/ @dermotduffy
|
||||
/homeassistant/components/motionmount/ @RJPoelstra
|
||||
/tests/components/motionmount/ @RJPoelstra
|
||||
/homeassistant/components/mqtt/ @emontnemery @jbouwh @bdraco
|
||||
/tests/components/mqtt/ @emontnemery @jbouwh @bdraco
|
||||
/homeassistant/components/mqtt/ @emontnemery @jbouwh
|
||||
/tests/components/mqtt/ @emontnemery @jbouwh
|
||||
/homeassistant/components/msteams/ @peroyvind
|
||||
/homeassistant/components/mullvad/ @meichthys
|
||||
/tests/components/mullvad/ @meichthys
|
||||
@@ -973,8 +927,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/octoprint/ @rfleming71
|
||||
/tests/components/octoprint/ @rfleming71
|
||||
/homeassistant/components/ohmconnect/ @robbiet480
|
||||
/homeassistant/components/ollama/ @synesthesiam
|
||||
/tests/components/ollama/ @synesthesiam
|
||||
/homeassistant/components/ombi/ @larssont
|
||||
/homeassistant/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
|
||||
/homeassistant/components/philips_js/ @elupus
|
||||
/tests/components/philips_js/ @elupus
|
||||
/homeassistant/components/pi_hole/ @shenxn
|
||||
/tests/components/pi_hole/ @shenxn
|
||||
/homeassistant/components/pi_hole/ @johnluetke @shenxn
|
||||
/tests/components/pi_hole/ @johnluetke @shenxn
|
||||
/homeassistant/components/picnic/ @corneyl
|
||||
/tests/components/picnic/ @corneyl
|
||||
/homeassistant/components/pilight/ @trekky12
|
||||
@@ -1096,8 +1048,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue
|
||||
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||
/tests/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||
/homeassistant/components/qingping/ @bdraco
|
||||
/tests/components/qingping/ @bdraco
|
||||
/homeassistant/components/qingping/ @bdraco @skgsergio
|
||||
/tests/components/qingping/ @bdraco @skgsergio
|
||||
/homeassistant/components/qld_bushfire/ @exxamalte
|
||||
/tests/components/qld_bushfire/ @exxamalte
|
||||
/homeassistant/components/qnap/ @disforw
|
||||
@@ -1196,13 +1148,11 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ruuvitag_ble/ @akx
|
||||
/homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc
|
||||
/tests/components/rympro/ @OnFreund @elad-bar @maorcc
|
||||
/homeassistant/components/sabnzbd/ @shaiu @jpbede
|
||||
/tests/components/sabnzbd/ @shaiu @jpbede
|
||||
/homeassistant/components/sabnzbd/ @shaiu
|
||||
/tests/components/sabnzbd/ @shaiu
|
||||
/homeassistant/components/saj/ @fredericvl
|
||||
/homeassistant/components/samsungtv/ @chemelli74 @epenet
|
||||
/tests/components/samsungtv/ @chemelli74 @epenet
|
||||
/homeassistant/components/sanix/ @tomaszsluszniak
|
||||
/tests/components/sanix/ @tomaszsluszniak
|
||||
/homeassistant/components/scene/ @home-assistant/core
|
||||
/tests/components/scene/ @home-assistant/core
|
||||
/homeassistant/components/schedule/ @home-assistant/core
|
||||
@@ -1281,21 +1231,20 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/smappee/ @bsmappee
|
||||
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
||||
/tests/components/smart_meter_texas/ @grahamwetzler
|
||||
/homeassistant/components/smartthings/ @andrewsayre
|
||||
/tests/components/smartthings/ @andrewsayre
|
||||
/homeassistant/components/smarttub/ @mdz
|
||||
/tests/components/smarttub/ @mdz
|
||||
/homeassistant/components/smarty/ @z0mbieprocess
|
||||
/homeassistant/components/smhi/ @gjohansson-ST
|
||||
/tests/components/smhi/ @gjohansson-ST
|
||||
/homeassistant/components/sms/ @ocalvo
|
||||
/tests/components/sms/ @ocalvo
|
||||
/homeassistant/components/snapcast/ @luar123
|
||||
/tests/components/snapcast/ @luar123
|
||||
/homeassistant/components/snmp/ @nmaggioni
|
||||
/tests/components/snmp/ @nmaggioni
|
||||
/homeassistant/components/snooz/ @AustinBrunkhorst
|
||||
/tests/components/snooz/ @AustinBrunkhorst
|
||||
/homeassistant/components/solaredge/ @frenck @bdraco
|
||||
/tests/components/solaredge/ @frenck @bdraco
|
||||
/homeassistant/components/solaredge/ @frenck
|
||||
/tests/components/solaredge/ @frenck
|
||||
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
||||
/homeassistant/components/solarlog/ @Ernst79
|
||||
/tests/components/solarlog/ @Ernst79
|
||||
@@ -1307,8 +1256,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/sonarr/ @ctalkington
|
||||
/homeassistant/components/songpal/ @rytilahti @shenxn
|
||||
/tests/components/songpal/ @rytilahti @shenxn
|
||||
/homeassistant/components/sonos/ @jjlawren @peterager
|
||||
/tests/components/sonos/ @jjlawren @peterager
|
||||
/homeassistant/components/sonos/ @jjlawren
|
||||
/tests/components/sonos/ @jjlawren
|
||||
/homeassistant/components/soundtouch/ @kroimon
|
||||
/tests/components/soundtouch/ @kroimon
|
||||
/homeassistant/components/spaceapi/ @fabaff
|
||||
@@ -1367,8 +1316,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/switchbee/ @jafar-atili
|
||||
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland
|
||||
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland
|
||||
/homeassistant/components/switchbot_cloud/ @SeraphicRav
|
||||
/tests/components/switchbot_cloud/ @SeraphicRav
|
||||
/homeassistant/components/switcher_kis/ @thecode
|
||||
/tests/components/switcher_kis/ @thecode
|
||||
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
|
||||
@@ -1421,8 +1370,7 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/thermobeacon/ @bdraco
|
||||
/homeassistant/components/thermopro/ @bdraco @h3ss
|
||||
/tests/components/thermopro/ @bdraco @h3ss
|
||||
/homeassistant/components/thethingsnetwork/ @angelnu
|
||||
/tests/components/thethingsnetwork/ @angelnu
|
||||
/homeassistant/components/thethingsnetwork/ @fabaff
|
||||
/homeassistant/components/thread/ @home-assistant/core
|
||||
/tests/components/thread/ @home-assistant/core
|
||||
/homeassistant/components/tibber/ @danielhiversen
|
||||
@@ -1486,8 +1434,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/unifi/ @Kane610
|
||||
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
||||
/homeassistant/components/unifiled/ @florisvdk
|
||||
/homeassistant/components/unifiprotect/ @bdraco
|
||||
/tests/components/unifiprotect/ @bdraco
|
||||
/homeassistant/components/unifiprotect/ @AngellusMortis @bdraco
|
||||
/tests/components/unifiprotect/ @AngellusMortis @bdraco
|
||||
/homeassistant/components/upb/ @gwww
|
||||
/tests/components/upb/ @gwww
|
||||
/homeassistant/components/upc_connect/ @pvizeli @fabaff
|
||||
@@ -1593,8 +1541,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/wiz/ @sbidy
|
||||
/homeassistant/components/wled/ @frenck
|
||||
/tests/components/wled/ @frenck
|
||||
/homeassistant/components/wolflink/ @adamkrol93 @mtielen
|
||||
/tests/components/wolflink/ @adamkrol93 @mtielen
|
||||
/homeassistant/components/wolflink/ @adamkrol93
|
||||
/tests/components/wolflink/ @adamkrol93
|
||||
/homeassistant/components/workday/ @fabaff @gjohansson-ST
|
||||
/tests/components/workday/ @fabaff @gjohansson-ST
|
||||
/homeassistant/components/worldclock/ @fabaff
|
||||
|
@@ -5,7 +5,7 @@
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
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
|
||||
and orientation.
|
||||
|
||||
|
32
Dockerfile
32
Dockerfile
@@ -6,43 +6,47 @@ FROM ${BUILD_FROM}
|
||||
|
||||
# Synchronize with homeassistant/core.py:async_stop
|
||||
ENV \
|
||||
S6_SERVICES_GRACETIME=240000 \
|
||||
UV_SYSTEM_PYTHON=true
|
||||
S6_SERVICES_GRACETIME=240000
|
||||
|
||||
ARG QEMU_CPU
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.1.43
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
## Setup Home Assistant Core dependencies
|
||||
COPY requirements.txt homeassistant/
|
||||
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
|
||||
RUN \
|
||||
uv pip install \
|
||||
--no-build \
|
||||
pip3 install \
|
||||
--only-binary=:all: \
|
||||
-r homeassistant/requirements.txt
|
||||
|
||||
COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/
|
||||
RUN \
|
||||
if ls homeassistant/home_assistant_*.whl 1> /dev/null 2>&1; then \
|
||||
uv pip install homeassistant/home_assistant_*.whl; \
|
||||
if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \
|
||||
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 \
|
||||
&& if [ "${BUILD_ARCH}" = "i386" ]; then \
|
||||
linux32 uv pip install \
|
||||
--no-build \
|
||||
LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \
|
||||
MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \
|
||||
linux32 pip3 install \
|
||||
--only-binary=:all: \
|
||||
-r homeassistant/requirements_all.txt; \
|
||||
else \
|
||||
uv pip install \
|
||||
--no-build \
|
||||
LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \
|
||||
MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \
|
||||
pip3 install \
|
||||
--only-binary=:all: \
|
||||
-r homeassistant/requirements_all.txt; \
|
||||
fi
|
||||
|
||||
## Setup Home Assistant Core
|
||||
COPY . homeassistant/
|
||||
RUN \
|
||||
uv pip install \
|
||||
pip3 install \
|
||||
--only-binary=:all: \
|
||||
-e ./homeassistant \
|
||||
&& python3 -m compileall \
|
||||
homeassistant/homeassistant
|
||||
|
@@ -22,7 +22,6 @@ RUN \
|
||||
libavcodec-dev \
|
||||
libavdevice-dev \
|
||||
libavutil-dev \
|
||||
libgammu-dev \
|
||||
libswscale-dev \
|
||||
libswresample-dev \
|
||||
libavfilter-dev \
|
||||
@@ -35,30 +34,21 @@ RUN \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
# Setup 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
|
||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||
RUN uv venv $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
WORKDIR /tmp
|
||||
WORKDIR /workspaces
|
||||
|
||||
# Install Python dependencies from requirements
|
||||
COPY requirements.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 ./
|
||||
RUN uv pip install -r requirements_test.txt
|
||||
|
||||
WORKDIR /workspaces
|
||||
RUN pip3 install -r requirements_test.txt
|
||||
RUN rm -rf requirements.txt requirements_test.txt requirements_test_pre_commit.txt homeassistant/
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
ENV SHELL /bin/bash
|
||||
|
@@ -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/>`__,
|
||||
`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|
|
||||
|
||||
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
|
||||
:target: https://demo.home-assistant.io
|
||||
.. |screenshot-integrations| image:: https://raw.githubusercontent.com/home-assistant/core/dev/.github/assets/screenshot-integrations.png
|
||||
:target: https://home-assistant.io/integrations/
|
||||
:target: https://home-assistant.io/integrations/
|
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.03.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.03.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.03.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.03.0
|
||||
i386: ghcr.io/home-assistant/i386-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.02.1
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.02.1
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.02.1
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.02.1
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from contextlib import suppress
|
||||
import faulthandler
|
||||
import os
|
||||
import sys
|
||||
@@ -147,7 +146,9 @@ def get_arguments() -> argparse.Namespace:
|
||||
help="Skips validation of operating system",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
arguments = parser.parse_args()
|
||||
|
||||
return arguments
|
||||
|
||||
|
||||
def check_threads() -> None:
|
||||
@@ -209,10 +210,8 @@ def main() -> int:
|
||||
exit_code = runner.run(runtime_conf)
|
||||
faulthandler.disable()
|
||||
|
||||
# It's possible for the fault file to disappear, so suppress obvious errors
|
||||
with suppress(FileNotFoundError):
|
||||
if os.path.getsize(fault_file_name) == 0:
|
||||
os.remove(fault_file_name)
|
||||
if os.path.getsize(fault_file_name) == 0:
|
||||
os.remove(fault_file_name)
|
||||
|
||||
check_threads()
|
||||
|
||||
|
@@ -33,9 +33,9 @@ EVENT_USER_ADDED = "user_added"
|
||||
EVENT_USER_UPDATED = "user_updated"
|
||||
EVENT_USER_REMOVED = "user_removed"
|
||||
|
||||
type _MfaModuleDict = dict[str, MultiFactorAuthModule]
|
||||
type _ProviderKey = tuple[str, str | None]
|
||||
type _ProviderDict = dict[_ProviderKey, AuthProvider]
|
||||
_MfaModuleDict = dict[str, MultiFactorAuthModule]
|
||||
_ProviderKey = tuple[str, str | None]
|
||||
_ProviderDict = dict[_ProviderKey, AuthProvider]
|
||||
|
||||
|
||||
class InvalidAuthError(Exception):
|
||||
@@ -85,7 +85,7 @@ async def auth_manager_from_config(
|
||||
module_hash[module.id] = module
|
||||
|
||||
manager = AuthManager(hass, store, provider_hash, module_hash)
|
||||
await manager.async_setup()
|
||||
manager.async_setup()
|
||||
return manager
|
||||
|
||||
|
||||
@@ -181,7 +181,8 @@ class AuthManager:
|
||||
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."""
|
||||
hass = self.hass
|
||||
hass.async_add_shutdown_job(
|
||||
@@ -516,13 +517,6 @@ class AuthManager:
|
||||
for revoke_callback in callbacks:
|
||||
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
|
||||
def _async_remove_expired_refresh_tokens(self, _: datetime | None = None) -> None:
|
||||
"""Remove expired refresh tokens."""
|
||||
|
@@ -62,7 +62,6 @@ class AuthStore:
|
||||
self._store = Store[dict[str, list[dict[str, Any]]]](
|
||||
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]:
|
||||
"""Retrieve all users."""
|
||||
@@ -136,10 +135,7 @@ class AuthStore:
|
||||
|
||||
async def async_remove_user(self, user: models.User) -> None:
|
||||
"""Remove a user."""
|
||||
user = 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._users.pop(user.id)
|
||||
self._async_schedule_save()
|
||||
|
||||
async def async_update_user(
|
||||
@@ -222,9 +218,7 @@ class AuthStore:
|
||||
kwargs["client_icon"] = client_icon
|
||||
|
||||
refresh_token = models.RefreshToken(**kwargs)
|
||||
token_id = refresh_token.id
|
||||
user.refresh_tokens[token_id] = refresh_token
|
||||
self._token_id_to_user_id[token_id] = user.id
|
||||
user.refresh_tokens[refresh_token.id] = refresh_token
|
||||
|
||||
self._async_schedule_save()
|
||||
return refresh_token
|
||||
@@ -232,17 +226,19 @@ class AuthStore:
|
||||
@callback
|
||||
def async_remove_refresh_token(self, refresh_token: models.RefreshToken) -> None:
|
||||
"""Remove a refresh token."""
|
||||
refresh_token_id = refresh_token.id
|
||||
if user_id := self._token_id_to_user_id.get(refresh_token_id):
|
||||
del self._users[user_id].refresh_tokens[refresh_token_id]
|
||||
del self._token_id_to_user_id[refresh_token_id]
|
||||
self._async_schedule_save()
|
||||
for user in self._users.values():
|
||||
if user.refresh_tokens.pop(refresh_token.id, None):
|
||||
self._async_schedule_save()
|
||||
break
|
||||
|
||||
@callback
|
||||
def async_get_refresh_token(self, token_id: str) -> models.RefreshToken | None:
|
||||
"""Get refresh token by id."""
|
||||
if user_id := self._token_id_to_user_id.get(token_id):
|
||||
return self._users[user_id].refresh_tokens.get(token_id)
|
||||
for user in self._users.values():
|
||||
refresh_token = user.refresh_tokens.get(token_id)
|
||||
if refresh_token is not None:
|
||||
return refresh_token
|
||||
|
||||
return None
|
||||
|
||||
@callback
|
||||
@@ -281,21 +277,6 @@ class AuthStore:
|
||||
)
|
||||
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
|
||||
"""Load the users."""
|
||||
if self._loaded:
|
||||
@@ -309,6 +290,8 @@ class AuthStore:
|
||||
perm_lookup = PermissionLookup(ent_reg, dev_reg)
|
||||
self._perm_lookup = perm_lookup
|
||||
|
||||
now_ts = dt_util.utcnow().timestamp()
|
||||
|
||||
if data is None or not isinstance(data, dict):
|
||||
self._set_defaults()
|
||||
return
|
||||
@@ -462,6 +445,14 @@ class AuthStore:
|
||||
else:
|
||||
last_used_at = None
|
||||
|
||||
if (
|
||||
expire_at := rt_dict.get("expire_at")
|
||||
) is None and token_type == models.TOKEN_TYPE_NORMAL:
|
||||
if last_used_at:
|
||||
expire_at = last_used_at.timestamp() + REFRESH_TOKEN_EXPIRATION
|
||||
else:
|
||||
expire_at = now_ts + REFRESH_TOKEN_EXPIRATION
|
||||
|
||||
token = models.RefreshToken(
|
||||
id=rt_dict["id"],
|
||||
user=users[rt_dict["user_id"]],
|
||||
@@ -478,7 +469,7 @@ class AuthStore:
|
||||
jwt_key=rt_dict["jwt_key"],
|
||||
last_used_at=last_used_at,
|
||||
last_used_ip=rt_dict.get("last_used_ip"),
|
||||
expire_at=rt_dict.get("expire_at"),
|
||||
expire_at=expire_at,
|
||||
version=rt_dict.get("version"),
|
||||
)
|
||||
if "credential_id" in rt_dict:
|
||||
@@ -487,17 +478,8 @@ class AuthStore:
|
||||
|
||||
self._groups = groups
|
||||
self._users = users
|
||||
self._build_token_id_to_user_id()
|
||||
self._async_schedule_save(INITIAL_LOAD_SAVE_DELAY)
|
||||
|
||||
@callback
|
||||
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
|
||||
}
|
||||
self._async_schedule_save(INITIAL_LOAD_SAVE_DELAY)
|
||||
|
||||
@callback
|
||||
def _async_schedule_save(self, delay: float = DEFAULT_SAVE_DELAY) -> None:
|
||||
@@ -592,7 +574,6 @@ class AuthStore:
|
||||
read_only_group = _system_read_only_group()
|
||||
groups[read_only_group.id] = read_only_group
|
||||
self._groups = groups
|
||||
self._build_token_id_to_user_id()
|
||||
|
||||
|
||||
def _system_admin_group() -> models.Group:
|
||||
|
@@ -78,7 +78,7 @@ class _PyJWTWithVerify(PyJWT):
|
||||
key: str,
|
||||
algorithms: list[str],
|
||||
issuer: str | None = None,
|
||||
leeway: float | timedelta = 0,
|
||||
leeway: int | float | timedelta = 0,
|
||||
options: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Verify a JWT's signature and claims."""
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import types
|
||||
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.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.importlib import async_import_module
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry()
|
||||
|
||||
@@ -30,7 +29,7 @@ MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema(
|
||||
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__)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
try:
|
||||
module = await async_import_module(hass, module_path)
|
||||
module = importlib.import_module(module_path)
|
||||
except ImportError as err:
|
||||
_LOGGER.error("Unable to load mfa module %s: %s", module_name, err)
|
||||
raise HomeAssistantError(
|
||||
|
@@ -88,7 +88,7 @@ class NotifySetting:
|
||||
target: str | None = attr.ib(default=None)
|
||||
|
||||
|
||||
type _UsersDict = dict[str, NotifySetting]
|
||||
_UsersDict = dict[str, NotifySetting]
|
||||
|
||||
|
||||
@MULTI_FACTOR_AUTH_MODULES.register("notify")
|
||||
|
@@ -3,9 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from functools import cached_property
|
||||
import secrets
|
||||
from typing import Any, NamedTuple
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||
import uuid
|
||||
|
||||
import attr
|
||||
@@ -19,6 +18,12 @@ from homeassistant.util import dt as dt_util
|
||||
from . import permissions as perm_mdl
|
||||
from .const import GROUP_ID_ADMIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from functools import cached_property
|
||||
else:
|
||||
from homeassistant.backports.functools import cached_property
|
||||
|
||||
|
||||
TOKEN_TYPE_NORMAL = "normal"
|
||||
TOKEN_TYPE_SYSTEM = "system"
|
||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
|
||||
@@ -86,7 +91,11 @@ class User:
|
||||
def invalidate_cache(self) -> None:
|
||||
"""Invalidate permission and is_admin cache."""
|
||||
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)
|
||||
|
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -63,7 +64,7 @@ class PolicyPermissions(AbstractPermissions):
|
||||
"""Return a function that can test entity access."""
|
||||
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."""
|
||||
return isinstance(other, PolicyPermissions) and other._policy == self._policy
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Final
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.const import (
|
||||
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.entity_registry import EVENT_ENTITY_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
|
||||
# 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_COMPONENT_LOADED,
|
||||
EVENT_CORE_CONFIG_UPDATE,
|
||||
|
@@ -58,7 +58,10 @@ def _merge_policies(sources: list[CategoryType]) -> CategoryType:
|
||||
continue
|
||||
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)
|
||||
|
||||
|
@@ -4,17 +4,17 @@ from collections.abc import Mapping
|
||||
|
||||
# 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 }
|
||||
Mapping[str, bool] | bool | None
|
||||
)
|
||||
|
||||
# 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
|
||||
Mapping[str, SubCategoryType]
|
||||
# Example: entities.all
|
||||
@@ -24,4 +24,4 @@ type CategoryType = (
|
||||
)
|
||||
|
||||
# Example: { entities: … }
|
||||
type PolicyType = Mapping[str, CategoryType]
|
||||
PolicyType = Mapping[str, CategoryType]
|
||||
|
@@ -10,8 +10,8 @@ from .const import SUBCAT_ALL
|
||||
from .models import PermissionLookup
|
||||
from .types import CategoryType, SubCategoryDict, ValueType
|
||||
|
||||
type LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], ValueType | None]
|
||||
type SubCatLookupType = dict[str, LookupFunc]
|
||||
LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], ValueType | None]
|
||||
SubCatLookupType = dict[str, LookupFunc]
|
||||
|
||||
|
||||
def lookup_all(
|
||||
|
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import importlib
|
||||
import logging
|
||||
import types
|
||||
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.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.importlib import async_import_module
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from ..auth_store import AuthStore
|
||||
from ..const import MFA_SESSION_EXPIRATION
|
||||
from ..models import AuthFlowResult, Credentials, RefreshToken, User, UserMeta
|
||||
|
||||
_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()
|
||||
|
||||
@@ -158,9 +157,7 @@ async def load_auth_provider_module(
|
||||
) -> types.ModuleType:
|
||||
"""Load an auth provider."""
|
||||
try:
|
||||
module = await async_import_module(
|
||||
hass, f"homeassistant.auth.providers.{provider}"
|
||||
)
|
||||
module = importlib.import_module(f"homeassistant.auth.providers.{provider}")
|
||||
except ImportError as err:
|
||||
_LOGGER.error("Unable to load auth provider %s: %s", provider, err)
|
||||
raise HomeAssistantError(
|
||||
|
@@ -28,8 +28,8 @@ from .. import InvalidAuthError
|
||||
from ..models import AuthFlowResult, Credentials, RefreshToken, UserMeta
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
|
||||
type IPAddress = IPv4Address | IPv6Address
|
||||
type IPNetwork = IPv4Network | IPv6Network
|
||||
IPAddress = IPv4Address | IPv6Address
|
||||
IPNetwork = IPv4Network | IPv6Network
|
||||
|
||||
CONF_TRUSTED_NETWORKS = "trusted_networks"
|
||||
CONF_TRUSTED_USERS = "trusted_users"
|
||||
|
@@ -9,21 +9,8 @@ import it.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum as _StrEnum
|
||||
from functools import partial
|
||||
from enum import StrEnum
|
||||
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedAlias,
|
||||
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())
|
||||
__all__ = [
|
||||
"StrEnum",
|
||||
]
|
||||
|
@@ -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.
|
||||
|
||||
Since we have dropped support for Python 3.11, we can remove this backport.
|
||||
This file is kept for now to avoid breaking custom components that might
|
||||
import it.
|
||||
"""
|
||||
# This file contains parts of Python's module wrapper
|
||||
# for the _functools C module
|
||||
# to allow utilities written in Python to be added
|
||||
# to the functools module.
|
||||
# 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 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 (
|
||||
DeprecatedAlias,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
_T = TypeVar("_T")
|
||||
|
||||
# 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())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
class cached_property(Generic[_T]):
|
||||
"""Backport of Python 3.12's cached_property.
|
||||
|
||||
Includes https://github.com/python/cpython/pull/101890/files
|
||||
"""
|
||||
|
||||
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]
|
||||
|
@@ -1,80 +1,21 @@
|
||||
"""Block blocking calls being done in asyncio."""
|
||||
|
||||
import builtins
|
||||
from contextlib import suppress
|
||||
from http.client import HTTPConnection
|
||||
import importlib
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from .helpers.frame import get_current_frame
|
||||
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
|
||||
from .util.async_ import protect_loop
|
||||
|
||||
|
||||
def enable() -> None:
|
||||
"""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
|
||||
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
|
||||
time.sleep = protect_loop(
|
||||
time.sleep,
|
||||
strict=False,
|
||||
check_allowed=_check_sleep_call_allowed,
|
||||
loop_thread_id=loop_thread_id,
|
||||
)
|
||||
time.sleep = protect_loop(time.sleep, strict=False)
|
||||
|
||||
if not _IN_TESTS:
|
||||
# Prevent files being opened inside the event loop
|
||||
builtins.open = protect_loop( # type: ignore[assignment]
|
||||
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,
|
||||
)
|
||||
# Currently disabled. pytz doing I/O when getting timezone.
|
||||
# Prevent files being opened inside the event loop
|
||||
# builtins.open = protect_loop(builtins.open)
|
||||
|
@@ -3,14 +3,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
import contextlib
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import logging.handlers
|
||||
import mimetypes
|
||||
from operator import contains, itemgetter
|
||||
from operator import itemgetter
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
@@ -24,14 +21,7 @@ import cryptography.hazmat.backends.openssl.backend # noqa: F401
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
||||
from . import (
|
||||
block_async_io,
|
||||
config as conf_util,
|
||||
config_entries,
|
||||
core,
|
||||
loader,
|
||||
requirements,
|
||||
)
|
||||
from . import config as conf_util, config_entries, core, loader, requirements
|
||||
|
||||
# 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
|
||||
@@ -42,11 +32,9 @@ from .components import (
|
||||
api as api_pre_import, # noqa: F401
|
||||
auth as auth_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
|
||||
diagnostics as diagnostics_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
|
||||
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
|
||||
@@ -63,7 +51,6 @@ from .components import (
|
||||
)
|
||||
from .components.sensor import recorder as sensor_recorder # noqa: F401
|
||||
from .const import (
|
||||
BASE_PLATFORMS,
|
||||
FORMAT_DATETIME,
|
||||
KEY_DATA_LOGGING as DATA_LOGGING,
|
||||
REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
||||
@@ -73,7 +60,6 @@ from .const import (
|
||||
from .exceptions import HomeAssistantError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
category_registry,
|
||||
config_validation as cv,
|
||||
device_registry,
|
||||
entity,
|
||||
@@ -86,43 +72,29 @@ from .helpers import (
|
||||
template,
|
||||
translation,
|
||||
)
|
||||
from .helpers.dispatcher import async_dispatcher_send_internal
|
||||
from .helpers.storage import get_internal_store_manager
|
||||
from .helpers.system_info import async_get_system_info
|
||||
from .helpers.dispatcher import async_dispatcher_send
|
||||
from .helpers.typing import ConfigType
|
||||
from .setup import (
|
||||
# _setup_started is marked as protected to make it clear
|
||||
# that it is not part of the public API and should not be used
|
||||
# by integrations. It is only used for internal tracking of
|
||||
# which integrations are being set up.
|
||||
_setup_started,
|
||||
async_get_setup_timings,
|
||||
BASE_PLATFORMS,
|
||||
DATA_SETUP_STARTED,
|
||||
DATA_SETUP_TIME,
|
||||
async_notify_setup_error,
|
||||
async_set_domains_to_be_loaded,
|
||||
async_setup_component,
|
||||
)
|
||||
from .util.async_ import create_eager_task
|
||||
from .util.hass_dict import HassKey
|
||||
from .util.logging import async_activate_log_queue_handler
|
||||
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:
|
||||
from .runner import RuntimeConfig
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SETUP_ORDER_SORT_KEY = partial(contains, BASE_PLATFORMS)
|
||||
|
||||
|
||||
ERROR_LOG_FILENAME = "home-assistant.log"
|
||||
|
||||
# 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
|
||||
SLOW_STARTUP_CHECK_INTERVAL = 1
|
||||
@@ -213,36 +185,16 @@ CRITICAL_INTEGRATIONS = {
|
||||
"frontend",
|
||||
}
|
||||
|
||||
SETUP_ORDER = (
|
||||
SETUP_ORDER = {
|
||||
# Load logging as soon as possible
|
||||
("logging", LOGGING_INTEGRATIONS),
|
||||
# Setup frontend and recorder
|
||||
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}),
|
||||
"logging": LOGGING_INTEGRATIONS,
|
||||
# Setup frontend
|
||||
"frontend": FRONTEND_INTEGRATIONS,
|
||||
# Setup recorder
|
||||
"recorder": RECORDER_INTEGRATIONS,
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
("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",
|
||||
]
|
||||
"debugger": DEBUGGER_INTEGRATIONS,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_hass(
|
||||
@@ -259,9 +211,6 @@ async def async_setup_hass(
|
||||
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.skip_pip = runtime_config.skip_pip
|
||||
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)
|
||||
|
||||
loader.async_setup(hass)
|
||||
block_async_io.enable()
|
||||
|
||||
config_dict = None
|
||||
basic_setup_success = False
|
||||
|
||||
@@ -325,7 +272,6 @@ async def async_setup_hass(
|
||||
hass = core.HomeAssistant(old_config.config_dir)
|
||||
if 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_packages = old_config.skip_pip_packages
|
||||
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:
|
||||
"""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:
|
||||
return
|
||||
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)
|
||||
entity.async_setup(hass)
|
||||
template.async_setup(hass)
|
||||
await asyncio.gather(
|
||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||
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(entity_registry.async_load(hass)),
|
||||
create_eager_task(floor_registry.async_load(hass)),
|
||||
create_eager_task(issue_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(restore_state.async_load(hass)),
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
# Set up core.
|
||||
@@ -432,11 +371,7 @@ async def async_from_config_dict(
|
||||
if not all(
|
||||
await asyncio.gather(
|
||||
*(
|
||||
create_eager_task(
|
||||
async_setup_component(hass, domain, config),
|
||||
name=f"bootstrap setup {domain}",
|
||||
loop=hass.loop,
|
||||
)
|
||||
create_eager_task(async_setup_component(hass, domain, config))
|
||||
for domain in CORE_INTEGRATIONS
|
||||
)
|
||||
)
|
||||
@@ -592,7 +527,7 @@ def async_enable_logging(
|
||||
err_log_path, when="midnight", backupCount=log_rotate_days
|
||||
)
|
||||
else:
|
||||
err_handler = _RotatingFileHandlerWithoutShouldRollOver(
|
||||
err_handler = logging.handlers.RotatingFileHandler(
|
||||
err_log_path, backupCount=1
|
||||
)
|
||||
|
||||
@@ -616,19 +551,6 @@ def async_enable_logging(
|
||||
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:
|
||||
"""Add local library to Python Path.
|
||||
|
||||
@@ -666,9 +588,7 @@ class _WatchPendingSetups:
|
||||
"""Periodic log and dispatch of setups that are pending."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: core.HomeAssistant,
|
||||
setup_started: dict[tuple[str, str | None], float],
|
||||
self, hass: core.HomeAssistant, setup_started: dict[str, float]
|
||||
) -> None:
|
||||
"""Initialize the WatchPendingSetups class."""
|
||||
self._hass = hass
|
||||
@@ -683,14 +603,13 @@ class _WatchPendingSetups:
|
||||
now = monotonic()
|
||||
self._duration_count += SLOW_STARTUP_CHECK_INTERVAL
|
||||
|
||||
remaining_with_setup_started: defaultdict[str, float] = defaultdict(float)
|
||||
for integration_group, start_time in self._setup_started.items():
|
||||
domain, _ = integration_group
|
||||
remaining_with_setup_started[domain] += now - start_time
|
||||
|
||||
remaining_with_setup_started = {
|
||||
domain: (now - start_time)
|
||||
for domain, start_time in self._setup_started.items()
|
||||
}
|
||||
if 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)
|
||||
self._async_dispatch(remaining_with_setup_started)
|
||||
if (
|
||||
@@ -701,7 +620,7 @@ class _WatchPendingSetups:
|
||||
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
|
||||
_LOGGER.warning(
|
||||
"Waiting on integrations to complete setup: %s",
|
||||
self._setup_started,
|
||||
", ".join(self._setup_started),
|
||||
)
|
||||
|
||||
_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:
|
||||
"""Dispatch the signal."""
|
||||
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._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."""
|
||||
# Avoid creating tasks for domains that were setup in a previous stage
|
||||
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 = {
|
||||
domain: hass.async_create_task_internal(
|
||||
domain: hass.async_create_task(
|
||||
async_setup_component(hass, domain, config),
|
||||
f"setup component {domain}",
|
||||
eager_start=True,
|
||||
)
|
||||
for domain in sorted(
|
||||
domains_not_yet_setup, key=SETUP_ORDER_SORT_KEY, reverse=True
|
||||
)
|
||||
for domain in domains_not_yet_setup
|
||||
}
|
||||
results = await asyncio.gather(*futures.values(), return_exceptions=True)
|
||||
for idx, domain in enumerate(futures):
|
||||
@@ -769,53 +683,29 @@ async def _async_resolve_domains_to_setup(
|
||||
hass: core.HomeAssistant, config: dict[str, Any]
|
||||
) -> tuple[set[str], dict[str, loader.Integration]]:
|
||||
"""Resolve all dependencies and return list of domains to set up."""
|
||||
base_platforms_loaded = False
|
||||
domains_to_setup = _get_domains(hass, config)
|
||||
needed_requirements: set[str] = set()
|
||||
platform_integrations = conf_util.extract_platform_integrations(
|
||||
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
|
||||
# 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] = {}
|
||||
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
|
||||
to_resolve = set()
|
||||
|
||||
if additional_manifests_to_load:
|
||||
to_get = {*old_to_resolve, *additional_manifests_to_load}
|
||||
additional_manifests_to_load.clear()
|
||||
if not base_platforms_loaded:
|
||||
# Load 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 resolve them at once
|
||||
base_platforms_loaded = True
|
||||
to_get = {*old_to_resolve, *BASE_PLATFORMS, *platform_integrations}
|
||||
else:
|
||||
to_get = old_to_resolve
|
||||
|
||||
@@ -828,17 +718,6 @@ async def _async_resolve_domains_to_setup(
|
||||
continue
|
||||
integration_cache[domain] = itg
|
||||
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:
|
||||
continue
|
||||
|
||||
@@ -887,12 +766,6 @@ async def _async_resolve_domains_to_setup(
|
||||
"check installed requirements",
|
||||
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
|
||||
# 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
|
||||
@@ -904,19 +777,10 @@ async def _async_resolve_domains_to_setup(
|
||||
# wait for the translation load lock, loading will be done by the
|
||||
# time it gets to it.
|
||||
hass.async_create_background_task(
|
||||
translation.async_load_integrations(hass, translations_to_load),
|
||||
"load translations",
|
||||
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]
|
||||
translation.async_load_integrations(
|
||||
hass, {*BASE_PLATFORMS, *platform_integrations, *domains_to_setup}
|
||||
),
|
||||
"preload storage",
|
||||
"load translations",
|
||||
eager_start=True,
|
||||
)
|
||||
|
||||
@@ -927,7 +791,11 @@ async def _async_set_up_integrations(
|
||||
hass: core.HomeAssistant, config: dict[str, Any]
|
||||
) -> None:
|
||||
"""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()
|
||||
|
||||
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:
|
||||
recorder.async_initialize_recorder(hass)
|
||||
|
||||
pre_stage_domains = [
|
||||
(name, domains_to_setup & domain_group) for name, domain_group in SETUP_ORDER
|
||||
]
|
||||
pre_stage_domains: dict[str, set[str]] = {
|
||||
name: domains_to_setup & domain_group
|
||||
for name, domain_group in SETUP_ORDER.items()
|
||||
}
|
||||
|
||||
# calculate what components to setup in what stage
|
||||
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
|
||||
|
||||
for name, domain_group in pre_stage_domains:
|
||||
for name, domain_group in pre_stage_domains.items():
|
||||
if domain_group:
|
||||
stage_2_domains -= 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)
|
||||
|
||||
# Enables after dependencies when setting up stage 1 domains
|
||||
@@ -994,7 +855,7 @@ async def _async_set_up_integrations(
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"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
|
||||
@@ -1010,7 +871,7 @@ async def _async_set_up_integrations(
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"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
|
||||
@@ -1021,14 +882,12 @@ async def _async_set_up_integrations(
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"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()
|
||||
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
setup_time = async_get_setup_timings(hass)
|
||||
_LOGGER.debug(
|
||||
"Integration setup times: %s",
|
||||
dict(sorted(setup_time.items(), key=itemgetter(1), reverse=True)),
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Integration setup times: %s",
|
||||
dict(sorted(setup_time.items(), key=itemgetter(1))),
|
||||
)
|
||||
|
5
homeassistant/brands/epson.json
Normal file
5
homeassistant/brands/epson.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "epson",
|
||||
"name": "Epson",
|
||||
"integrations": ["epson", "epsonworkforce"]
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "eq3",
|
||||
"name": "eQ-3",
|
||||
"integrations": ["maxcube", "eq3btsmart"]
|
||||
"integrations": ["maxcube"]
|
||||
}
|
||||
|
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "motionblinds",
|
||||
"name": "Motionblinds",
|
||||
"integrations": ["motion_blinds", "motionblinds_ble"]
|
||||
}
|
@@ -5,7 +5,9 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
|
||||
from jaraco.abode.automation import Automation as AbodeAuto
|
||||
from jaraco.abode.client import Client as Abode
|
||||
from jaraco.abode.devices.base import Device as AbodeDev
|
||||
from jaraco.abode.exceptions import (
|
||||
AuthenticationException as AbodeAuthenticationException,
|
||||
Exception as AbodeException,
|
||||
@@ -27,11 +29,11 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, ServiceCall
|
||||
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.typing import ConfigType
|
||||
|
||||
from .const import CONF_POLLING, DOMAIN, LOGGER
|
||||
from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER
|
||||
|
||||
SERVICE_SETTINGS = "change_setting"
|
||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||
@@ -81,12 +83,6 @@ class AbodeSystem:
|
||||
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:
|
||||
"""Set up Abode integration from a config entry."""
|
||||
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 setup_hass_events(hass)
|
||||
await hass.async_add_executor_job(setup_hass_services, hass)
|
||||
await hass.async_add_executor_job(setup_abode_events, hass)
|
||||
|
||||
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:
|
||||
"""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)
|
||||
|
||||
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}"
|
||||
dispatcher_send(hass, signal)
|
||||
|
||||
hass.services.async_register(
|
||||
hass.services.register(
|
||||
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
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
hass.services.register(
|
||||
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(
|
||||
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()
|
||||
|
@@ -17,9 +17,8 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@@ -22,9 +22,8 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
|
||||
from . import AbodeSystem
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@@ -19,9 +19,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from . import AbodeSystem
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .entity import AbodeDevice
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
|
||||
|
@@ -10,9 +10,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@@ -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()
|
@@ -5,10 +5,5 @@
|
||||
"default": "mdi:robot"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"capture_image": "mdi:camera",
|
||||
"change_setting": "mdi:cog",
|
||||
"trigger_automation": "mdi:play"
|
||||
}
|
||||
}
|
||||
|
@@ -23,9 +23,8 @@ from homeassistant.util.color import (
|
||||
color_temperature_mired_to_kelvin,
|
||||
)
|
||||
|
||||
from . import AbodeSystem
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@@ -10,9 +10,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@@ -27,9 +27,8 @@ from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
ABODE_TEMPERATURE_UNIT_HA_UNIT = {
|
||||
UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,
|
||||
|
@@ -13,9 +13,8 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AbodeSystem
|
||||
from . import AbodeAutomation, AbodeDevice, AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeAutomation, AbodeDevice
|
||||
|
||||
DEVICE_TYPES = [TYPE_SWITCH, TYPE_VALVE]
|
||||
|
||||
|
@@ -2,10 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
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.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.helpers import entity_registry as er
|
||||
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 .coordinator import (
|
||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||
AccuWeatherObservationDataUpdateCoordinator,
|
||||
)
|
||||
from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
|
||||
|
||||
@dataclass
|
||||
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:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up AccuWeather as config entry."""
|
||||
api_key: str = entry.data[CONF_API_KEY]
|
||||
name: str = entry.data[CONF_NAME]
|
||||
|
||||
assert entry.unique_id is not None
|
||||
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)
|
||||
accuweather = AccuWeather(api_key, websession, location_key=location_key)
|
||||
|
||||
coordinator_observation = AccuWeatherObservationDataUpdateCoordinator(
|
||||
hass,
|
||||
accuweather,
|
||||
name,
|
||||
"observation",
|
||||
UPDATE_INTERVAL_OBSERVATION,
|
||||
coordinator = AccuWeatherDataUpdateCoordinator(
|
||||
hass, websession, api_key, location_key, forecast, name
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
coordinator_daily_forecast = AccuWeatherDailyForecastDataUpdateCoordinator(
|
||||
hass,
|
||||
accuweather,
|
||||
name,
|
||||
"daily forecast",
|
||||
UPDATE_INTERVAL_DAILY_FORECAST,
|
||||
)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
await coordinator_observation.async_config_entry_first_refresh()
|
||||
await coordinator_daily_forecast.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = AccuWeatherData(
|
||||
coordinator_observation=coordinator_observation,
|
||||
coordinator_daily_forecast=coordinator_daily_forecast,
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# Remove ozone sensors from registry if they exist
|
||||
ent_reg = er.async_get(hass)
|
||||
for day in range(5):
|
||||
unique_id = f"{location_key}-ozone-{day}"
|
||||
for day in range(0, 5):
|
||||
unique_id = f"{coordinator.location_key}-ozone-{day}"
|
||||
if entity_id := ent_reg.async_get_entity_id(SENSOR_PLATFORM, DOMAIN, unique_id):
|
||||
_LOGGER.debug("Removing ozone sensor entity %s", entity_id)
|
||||
ent_reg.async_remove(entity_id)
|
||||
@@ -85,8 +61,78 @@ async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: AccuWeatherConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""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}}
|
||||
|
@@ -10,12 +10,26 @@ from aiohttp import ClientError
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
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.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
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):
|
||||
@@ -73,3 +87,9 @@ class AccuWeatherFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler:
|
||||
"""Options callback for AccuWeather."""
|
||||
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
@@ -28,8 +27,10 @@ ATTR_CATEGORY: Final = "Category"
|
||||
ATTR_DIRECTION: Final = "Direction"
|
||||
ATTR_ENGLISH: Final = "English"
|
||||
ATTR_LEVEL: Final = "level"
|
||||
ATTR_FORECAST: Final = "forecast"
|
||||
ATTR_SPEED: Final = "Speed"
|
||||
ATTR_VALUE: Final = "Value"
|
||||
CONF_FORECAST: Final = "forecast"
|
||||
DOMAIN: Final = "accuweather"
|
||||
MANUFACTURER: Final = "AccuWeather, Inc."
|
||||
MAX_FORECAST_DAYS: Final = 4
|
||||
@@ -55,5 +56,3 @@ CONDITION_MAP = {
|
||||
for cond_ha, cond_codes in CONDITION_CLASSES.items()
|
||||
for cond_code in cond_codes
|
||||
}
|
||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
|
||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||
|
@@ -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}/"
|
||||
),
|
||||
)
|
@@ -5,21 +5,27 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
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.core import HomeAssistant
|
||||
|
||||
from . import AccuWeatherConfigEntry, AccuWeatherData
|
||||
from . import AccuWeatherDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
|
||||
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: AccuWeatherConfigEntry
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""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),
|
||||
"observation_data": accuweather_data.coordinator_observation.data,
|
||||
"coordinator_data": coordinator.data,
|
||||
}
|
||||
|
||||
return diagnostics_data
|
||||
|
@@ -8,6 +8,6 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["accuweather"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["accuweather==3.0.0"],
|
||||
"requirements": ["accuweather==2.1.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
PERCENTAGE,
|
||||
@@ -27,22 +28,20 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AccuWeatherConfigEntry
|
||||
from . import AccuWeatherDataUpdateCoordinator
|
||||
from .const import (
|
||||
API_METRIC,
|
||||
ATTR_CATEGORY,
|
||||
ATTR_DIRECTION,
|
||||
ATTR_ENGLISH,
|
||||
ATTR_FORECAST,
|
||||
ATTR_LEVEL,
|
||||
ATTR_SPEED,
|
||||
ATTR_VALUE,
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
MAX_FORECAST_DAYS,
|
||||
)
|
||||
from .coordinator import (
|
||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||
AccuWeatherObservationDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -53,18 +52,12 @@ class AccuWeatherSensorDescription(SensorEntityDescription):
|
||||
|
||||
value_fn: Callable[[dict[str, Any]], str | int | float | None]
|
||||
attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {}
|
||||
day: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AccuWeatherForecastSensorDescription(AccuWeatherSensorDescription):
|
||||
"""Class describing AccuWeather sensor entities."""
|
||||
|
||||
day: int
|
||||
|
||||
|
||||
FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="AirQuality",
|
||||
icon="mdi:air-filter",
|
||||
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)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="CloudCoverDay",
|
||||
icon="mdi:weather-cloudy",
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -88,7 +81,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="CloudCoverNight",
|
||||
icon="mdi:weather-cloudy",
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -100,7 +93,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="Grass",
|
||||
icon="mdi:grass",
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -113,7 +106,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="HoursOfSun",
|
||||
icon="mdi:weather-partly-cloudy",
|
||||
native_unit_of_measurement=UnitOfTime.HOURS,
|
||||
@@ -124,7 +117,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="LongPhraseDay",
|
||||
value_fn=lambda data: cast(str, data),
|
||||
translation_key=f"condition_day_{day}d",
|
||||
@@ -133,7 +126,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="LongPhraseNight",
|
||||
value_fn=lambda data: cast(str, data),
|
||||
translation_key=f"condition_night_{day}d",
|
||||
@@ -142,7 +135,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="Mold",
|
||||
icon="mdi:blur",
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -155,7 +148,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="Ragweed",
|
||||
icon="mdi:sprout",
|
||||
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)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="RealFeelTemperatureMax",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
@@ -179,7 +172,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="RealFeelTemperatureMin",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
@@ -190,7 +183,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="RealFeelTemperatureShadeMax",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -202,7 +195,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="RealFeelTemperatureShadeMin",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -214,7 +207,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="SolarIrradianceDay",
|
||||
icon="mdi:weather-sunny",
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -226,7 +219,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="SolarIrradianceNight",
|
||||
icon="mdi:weather-sunny",
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -238,7 +231,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="ThunderstormProbabilityDay",
|
||||
icon="mdi:weather-lightning",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
@@ -249,7 +242,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="ThunderstormProbabilityNight",
|
||||
icon="mdi:weather-lightning",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
@@ -260,7 +253,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="Tree",
|
||||
icon="mdi:tree-outline",
|
||||
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)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="UVIndex",
|
||||
icon="mdi:weather-sunny",
|
||||
native_unit_of_measurement=UV_INDEX,
|
||||
@@ -285,7 +278,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="WindGustDay",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -298,7 +291,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="WindGustNight",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -311,7 +304,7 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
||||
for day in range(MAX_FORECAST_DAYS + 1)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="WindDay",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
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)
|
||||
),
|
||||
*(
|
||||
AccuWeatherForecastSensorDescription(
|
||||
AccuWeatherSensorDescription(
|
||||
key="WindNight",
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
@@ -456,36 +449,29 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AccuWeatherConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""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] = [
|
||||
AccuWeatherSensor(observation_coordinator, description)
|
||||
for description in SENSOR_TYPES
|
||||
coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
sensors = [
|
||||
AccuWeatherSensor(coordinator, description) for description in SENSOR_TYPES
|
||||
]
|
||||
|
||||
sensors.extend(
|
||||
[
|
||||
AccuWeatherForecastSensor(forecast_daily_coordinator, description)
|
||||
for description in FORECAST_SENSOR_TYPES
|
||||
if description.key in forecast_daily_coordinator.data[description.day]
|
||||
]
|
||||
)
|
||||
if coordinator.forecast:
|
||||
for description in FORECAST_SENSOR_TYPES:
|
||||
# Some air quality/allergy sensors are only available for certain
|
||||
# locations.
|
||||
if description.key not in coordinator.data[ATTR_FORECAST][description.day]:
|
||||
continue
|
||||
sensors.append(AccuWeatherSensor(coordinator, description))
|
||||
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class AccuWeatherSensor(
|
||||
CoordinatorEntity[AccuWeatherObservationDataUpdateCoordinator], SensorEntity
|
||||
CoordinatorEntity[AccuWeatherDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Define an AccuWeather entity."""
|
||||
|
||||
@@ -495,15 +481,22 @@ class AccuWeatherSensor(
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AccuWeatherObservationDataUpdateCoordinator,
|
||||
coordinator: AccuWeatherDataUpdateCoordinator,
|
||||
description: AccuWeatherSensorDescription,
|
||||
) -> 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._attr_unique_id = f"{coordinator.location_key}-{description.key}".lower()
|
||||
self._sensor_data = _get_sensor_data(
|
||||
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
|
||||
|
||||
@property
|
||||
@@ -514,78 +507,30 @@ class AccuWeatherSensor(
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""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)
|
||||
|
||||
@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.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._sensor_data = _get_sensor_data(
|
||||
self.coordinator.data, self.entity_description.key, self.forecast_day
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@staticmethod
|
||||
def _get_sensor_data(
|
||||
sensors: list[dict[str, Any]],
|
||||
kind: str,
|
||||
forecast_day: int,
|
||||
) -> Any:
|
||||
"""Get sensor data."""
|
||||
return sensors[forecast_day][kind]
|
||||
|
||||
def _get_sensor_data(
|
||||
sensors: dict[str, Any],
|
||||
kind: str,
|
||||
forecast_day: int | None = None,
|
||||
) -> Any:
|
||||
"""Get sensor data."""
|
||||
if forecast_day is not None:
|
||||
return sensors[ATTR_FORECAST][forecast_day][kind]
|
||||
|
||||
if kind == "Precipitation":
|
||||
return sensors["PrecipitationSummary"]["PastHour"]
|
||||
|
||||
return sensors[kind]
|
||||
|
@@ -11,7 +11,7 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"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": {
|
||||
"info": {
|
||||
"can_reach_server": "Reach AccuWeather server",
|
||||
|
@@ -9,7 +9,6 @@ from accuweather.const import ENDPOINT
|
||||
from homeassistant.components import system_health
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import AccuWeatherConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@@ -23,11 +22,9 @@ def async_register(
|
||||
|
||||
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"""Get info for the info page."""
|
||||
config_entry: AccuWeatherConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
|
||||
remaining_requests = (
|
||||
config_entry.runtime_data.coordinator_observation.accuweather.requests_remaining
|
||||
)
|
||||
remaining_requests = list(hass.data[DOMAIN].values())[
|
||||
0
|
||||
].accuweather.requests_remaining
|
||||
|
||||
return {
|
||||
"can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT),
|
||||
|
@@ -17,10 +17,11 @@ from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_UV_INDEX,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
CoordinatorWeatherEntity,
|
||||
Forecast,
|
||||
SingleCoordinatorWeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
UnitOfLength,
|
||||
UnitOfPrecipitationDepth,
|
||||
@@ -32,153 +33,125 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.dt import utc_from_timestamp
|
||||
|
||||
from . import AccuWeatherConfigEntry, AccuWeatherData
|
||||
from . import AccuWeatherDataUpdateCoordinator
|
||||
from .const import (
|
||||
API_METRIC,
|
||||
ATTR_DIRECTION,
|
||||
ATTR_FORECAST,
|
||||
ATTR_SPEED,
|
||||
ATTR_VALUE,
|
||||
ATTRIBUTION,
|
||||
CONDITION_MAP,
|
||||
)
|
||||
from .coordinator import (
|
||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||
AccuWeatherObservationDataUpdateCoordinator,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AccuWeatherConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""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(
|
||||
CoordinatorWeatherEntity[
|
||||
AccuWeatherObservationDataUpdateCoordinator,
|
||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||
]
|
||||
SingleCoordinatorWeatherEntity[AccuWeatherDataUpdateCoordinator]
|
||||
):
|
||||
"""Define an AccuWeather entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, accuweather_data: AccuWeatherData) -> None:
|
||||
def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
observation_coordinator=accuweather_data.coordinator_observation,
|
||||
daily_coordinator=accuweather_data.coordinator_daily_forecast,
|
||||
)
|
||||
|
||||
super().__init__(coordinator)
|
||||
self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
|
||||
self._attr_native_pressure_unit = UnitOfPressure.HPA
|
||||
self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
self._attr_native_visibility_unit = UnitOfLength.KILOMETERS
|
||||
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_device_info = accuweather_data.coordinator_observation.device_info
|
||||
self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
|
||||
|
||||
self.observation_coordinator = accuweather_data.coordinator_observation
|
||||
self.daily_coordinator = accuweather_data.coordinator_daily_forecast
|
||||
self._attr_device_info = coordinator.device_info
|
||||
if self.coordinator.forecast:
|
||||
self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
|
||||
|
||||
@property
|
||||
def condition(self) -> str | None:
|
||||
"""Return the current condition."""
|
||||
return CONDITION_MAP.get(self.observation_coordinator.data["WeatherIcon"])
|
||||
return CONDITION_MAP.get(self.coordinator.data["WeatherIcon"])
|
||||
|
||||
@property
|
||||
def cloud_coverage(self) -> float:
|
||||
"""Return the Cloud coverage in %."""
|
||||
return cast(float, self.observation_coordinator.data["CloudCover"])
|
||||
return cast(float, self.coordinator.data["CloudCover"])
|
||||
|
||||
@property
|
||||
def native_apparent_temperature(self) -> float:
|
||||
"""Return the apparent temperature."""
|
||||
return cast(
|
||||
float,
|
||||
self.observation_coordinator.data["ApparentTemperature"][API_METRIC][
|
||||
ATTR_VALUE
|
||||
],
|
||||
float, self.coordinator.data["ApparentTemperature"][API_METRIC][ATTR_VALUE]
|
||||
)
|
||||
|
||||
@property
|
||||
def native_temperature(self) -> float:
|
||||
"""Return the temperature."""
|
||||
return cast(
|
||||
float,
|
||||
self.observation_coordinator.data["Temperature"][API_METRIC][ATTR_VALUE],
|
||||
)
|
||||
return cast(float, self.coordinator.data["Temperature"][API_METRIC][ATTR_VALUE])
|
||||
|
||||
@property
|
||||
def native_pressure(self) -> float:
|
||||
"""Return the pressure."""
|
||||
return cast(
|
||||
float, self.observation_coordinator.data["Pressure"][API_METRIC][ATTR_VALUE]
|
||||
)
|
||||
return cast(float, self.coordinator.data["Pressure"][API_METRIC][ATTR_VALUE])
|
||||
|
||||
@property
|
||||
def native_dew_point(self) -> float:
|
||||
"""Return the dew point."""
|
||||
return cast(
|
||||
float, self.observation_coordinator.data["DewPoint"][API_METRIC][ATTR_VALUE]
|
||||
)
|
||||
return cast(float, self.coordinator.data["DewPoint"][API_METRIC][ATTR_VALUE])
|
||||
|
||||
@property
|
||||
def humidity(self) -> int:
|
||||
"""Return the humidity."""
|
||||
return cast(int, self.observation_coordinator.data["RelativeHumidity"])
|
||||
return cast(int, self.coordinator.data["RelativeHumidity"])
|
||||
|
||||
@property
|
||||
def native_wind_gust_speed(self) -> float:
|
||||
"""Return the wind gust speed."""
|
||||
return cast(
|
||||
float,
|
||||
self.observation_coordinator.data["WindGust"][ATTR_SPEED][API_METRIC][
|
||||
ATTR_VALUE
|
||||
],
|
||||
float, self.coordinator.data["WindGust"][ATTR_SPEED][API_METRIC][ATTR_VALUE]
|
||||
)
|
||||
|
||||
@property
|
||||
def native_wind_speed(self) -> float:
|
||||
"""Return the wind speed."""
|
||||
return cast(
|
||||
float,
|
||||
self.observation_coordinator.data["Wind"][ATTR_SPEED][API_METRIC][
|
||||
ATTR_VALUE
|
||||
],
|
||||
float, self.coordinator.data["Wind"][ATTR_SPEED][API_METRIC][ATTR_VALUE]
|
||||
)
|
||||
|
||||
@property
|
||||
def wind_bearing(self) -> int:
|
||||
"""Return the wind bearing."""
|
||||
return cast(
|
||||
int, self.observation_coordinator.data["Wind"][ATTR_DIRECTION]["Degrees"]
|
||||
)
|
||||
return cast(int, self.coordinator.data["Wind"][ATTR_DIRECTION]["Degrees"])
|
||||
|
||||
@property
|
||||
def native_visibility(self) -> float:
|
||||
"""Return the visibility."""
|
||||
return cast(
|
||||
float,
|
||||
self.observation_coordinator.data["Visibility"][API_METRIC][ATTR_VALUE],
|
||||
)
|
||||
return cast(float, self.coordinator.data["Visibility"][API_METRIC][ATTR_VALUE])
|
||||
|
||||
@property
|
||||
def uv_index(self) -> float:
|
||||
"""Return the UV index."""
|
||||
return cast(float, self.observation_coordinator.data["UVIndex"])
|
||||
return cast(float, self.coordinator.data["UVIndex"])
|
||||
|
||||
@callback
|
||||
def _async_forecast_daily(self) -> list[Forecast] | None:
|
||||
"""Return the daily forecast in native units."""
|
||||
@property
|
||||
def forecast(self) -> list[Forecast] | None:
|
||||
"""Return the forecast array."""
|
||||
if not self.coordinator.forecast:
|
||||
return None
|
||||
# remap keys from library to keys understood by the weather component
|
||||
return [
|
||||
{
|
||||
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_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
|
||||
|
@@ -4,35 +4,30 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .hub import PulseHub
|
||||
|
||||
CONF_HUBS = "hubs"
|
||||
|
||||
PLATFORMS = [Platform.COVER, Platform.SENSOR]
|
||||
|
||||
type AcmedaConfigEntry = ConfigEntry[PulseHub]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: AcmedaConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up Rollease Acmeda Automate hub from a config entry."""
|
||||
hub = PulseHub(hass, config_entry)
|
||||
|
||||
if not await hub.async_setup():
|
||||
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)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: AcmedaConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""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(
|
||||
config_entry, PLATFORMS
|
||||
@@ -41,4 +36,7 @@ async def async_unload_entry(
|
||||
if not await hub.async_reset():
|
||||
return False
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
@@ -9,23 +9,24 @@ from homeassistant.components.cover import (
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AcmedaConfigEntry
|
||||
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 .hub import PulseHub
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AcmedaConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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()
|
||||
|
||||
|
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiopulse import Roller
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -13,20 +11,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AcmedaConfigEntry
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_acmeda_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_class: type,
|
||||
config_entry: AcmedaConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
current: set[int],
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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)
|
||||
|
||||
api = hub.api.rollers
|
||||
|
@@ -3,24 +3,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AcmedaConfigEntry
|
||||
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 .hub import PulseHub
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AcmedaConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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()
|
||||
|
||||
|
@@ -9,7 +9,7 @@ from typing import Final
|
||||
|
||||
LEASES_REGEX: Final[re.Pattern[str]] = re.compile(
|
||||
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"\svalid\sfor:\s(?P<timevalid>(-?\d+))"
|
||||
r"\ssec"
|
||||
+ r"\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))"
|
||||
+ r"\svalid\sfor:\s(?P<timevalid>(-?\d+))"
|
||||
+ r"\ssec"
|
||||
)
|
||||
|
@@ -135,15 +135,11 @@ class AdaxDevice(ClimateEntity):
|
||||
class LocalAdaxDevice(ClimateEntity):
|
||||
"""Representation of a heater."""
|
||||
|
||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
|
||||
_attr_hvac_modes = [HVACMode.HEAT]
|
||||
_attr_hvac_mode = HVACMode.HEAT
|
||||
_attr_max_temp = 35
|
||||
_attr_min_temp = 5
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_target_temperature_step = PRECISION_WHOLE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
@@ -156,14 +152,6 @@ class LocalAdaxDevice(ClimateEntity):
|
||||
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:
|
||||
"""Set new target temperature."""
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
@@ -173,14 +161,6 @@ class LocalAdaxDevice(ClimateEntity):
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data."""
|
||||
data = await self._adax_data_handler.get_status()
|
||||
self._attr_target_temperature = data["target_temperature"]
|
||||
self._attr_current_temperature = data["current_temperature"]
|
||||
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
|
||||
|
@@ -2,12 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
@@ -26,6 +24,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import (
|
||||
CONF_FORCE,
|
||||
DATA_ADGUARD_CLIENT,
|
||||
DOMAIN,
|
||||
SERVICE_ADD_URL,
|
||||
SERVICE_DISABLE_URL,
|
||||
@@ -43,18 +42,9 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
||||
type AdGuardConfigEntry = ConfigEntry[AdGuardData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdGuardData:
|
||||
"""Adguard data type."""
|
||||
|
||||
client: AdGuardHome
|
||||
version: str
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up AdGuard Home from a config entry."""
|
||||
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
|
||||
adguard = AdGuardHome(
|
||||
@@ -67,13 +57,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> b
|
||||
session=session,
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_ADGUARD_CLIENT: adguard}
|
||||
|
||||
try:
|
||||
version = await adguard.version()
|
||||
await adguard.version()
|
||||
except AdGuardHomeConnectionError as exception:
|
||||
raise ConfigEntryNotReady from exception
|
||||
|
||||
entry.runtime_data = AdGuardData(adguard, version)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
async def add_url(call: ServiceCall) -> None:
|
||||
@@ -117,20 +107,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> b
|
||||
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_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
loaded_entries = [
|
||||
entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.state == ConfigEntryState.LOADED
|
||||
]
|
||||
if len(loaded_entries) == 1:
|
||||
# This is the last loaded instance of AdGuard, deregister any services
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
|
||||
del hass.data[DOMAIN]
|
||||
|
||||
return unload_ok
|
||||
|
@@ -6,6 +6,9 @@ DOMAIN = "adguard"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DATA_ADGUARD_CLIENT = "adguard_client"
|
||||
DATA_ADGUARD_VERSION = "adguard_version"
|
||||
|
||||
CONF_FORCE = "force"
|
||||
|
||||
SERVICE_ADD_URL = "add_url"
|
||||
|
@@ -2,14 +2,13 @@
|
||||
|
||||
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.entity import Entity
|
||||
|
||||
from . import AdGuardConfigEntry, AdGuardData
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER
|
||||
|
||||
|
||||
class AdGuardHomeEntity(Entity):
|
||||
@@ -20,13 +19,12 @@ class AdGuardHomeEntity(Entity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: AdGuardData,
|
||||
entry: AdGuardConfigEntry,
|
||||
adguard: AdGuardHome,
|
||||
entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the AdGuard Home entity."""
|
||||
self._entry = entry
|
||||
self.data = data
|
||||
self.adguard = data.client
|
||||
self.adguard = adguard
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
@@ -46,7 +44,7 @@ class AdGuardHomeEntity(Entity):
|
||||
|
||||
async def _adguard_update(self) -> None:
|
||||
"""Update AdGuard Home entity."""
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
@@ -70,6 +68,8 @@ class AdGuardHomeEntity(Entity):
|
||||
},
|
||||
manufacturer="AdGuard Team",
|
||||
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,
|
||||
)
|
||||
|
@@ -7,15 +7,16 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from adguardhome import AdGuardHome
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdGuardConfigEntry, AdGuardData
|
||||
from .const import DOMAIN
|
||||
from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN
|
||||
from .entity import AdGuardHomeEntity
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=300)
|
||||
@@ -84,14 +85,21 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AdGuardConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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(
|
||||
[AdGuardHomeSensor(data, entry, description) for description in SENSORS],
|
||||
[AdGuardHomeSensor(adguard, entry, description) for description in SENSORS],
|
||||
True,
|
||||
)
|
||||
|
||||
@@ -103,18 +111,18 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: AdGuardData,
|
||||
entry: AdGuardConfigEntry,
|
||||
adguard: AdGuardHome,
|
||||
entry: ConfigEntry,
|
||||
description: AdGuardHomeEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
super().__init__(data, entry)
|
||||
super().__init__(adguard, entry)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = "_".join(
|
||||
[
|
||||
DOMAIN,
|
||||
self.adguard.host,
|
||||
str(self.adguard.port),
|
||||
adguard.host,
|
||||
str(adguard.port),
|
||||
"sensor",
|
||||
description.key,
|
||||
]
|
||||
|
@@ -7,14 +7,15 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from adguardhome import AdGuardHome, AdGuardHomeError
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdGuardConfigEntry, AdGuardData
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN, LOGGER
|
||||
from .entity import AdGuardHomeEntity
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
@@ -78,14 +79,21 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AdGuardConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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(
|
||||
[AdGuardHomeSwitch(data, entry, description) for description in SWITCHES],
|
||||
[AdGuardHomeSwitch(adguard, entry, description) for description in SWITCHES],
|
||||
True,
|
||||
)
|
||||
|
||||
@@ -97,21 +105,15 @@ class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: AdGuardData,
|
||||
entry: AdGuardConfigEntry,
|
||||
adguard: AdGuardHome,
|
||||
entry: ConfigEntry,
|
||||
description: AdGuardHomeSwitchEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
super().__init__(data, entry)
|
||||
super().__init__(adguard, entry)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = "_".join(
|
||||
[
|
||||
DOMAIN,
|
||||
self.adguard.host,
|
||||
str(self.adguard.port),
|
||||
"switch",
|
||||
description.key,
|
||||
]
|
||||
[DOMAIN, adguard.host, str(adguard.port), "switch", description.key]
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
|
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"services": {
|
||||
"write_data_by_name": "mdi:pencil"
|
||||
}
|
||||
}
|
@@ -12,11 +12,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import ADVANTAGE_AIR_RETRY
|
||||
from .const import ADVANTAGE_AIR_RETRY, DOMAIN
|
||||
from .models import AdvantageAirData
|
||||
|
||||
type AdvantageAirDataConfigEntry = ConfigEntry[AdvantageAirData]
|
||||
|
||||
ADVANTAGE_AIR_SYNC_INTERVAL = 15
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -33,9 +31,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
REQUEST_REFRESH_DELAY = 0.5
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AdvantageAirDataConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Advantage Air config."""
|
||||
ip_address = entry.data[CONF_IP_ADDRESS]
|
||||
port = entry.data[CONF_PORT]
|
||||
@@ -65,15 +61,19 @@ async def async_setup_entry(
|
||||
|
||||
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)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: AdvantageAirDataConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""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
|
||||
|
@@ -6,11 +6,12 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
|
||||
from .models import AdvantageAirData
|
||||
|
||||
@@ -19,12 +20,12 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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] = []
|
||||
if aircons := instance.coordinator.data.get("aircons"):
|
||||
|
@@ -16,18 +16,19 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import (
|
||||
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
||||
ADVANTAGE_AIR_STATE_CLOSE,
|
||||
ADVANTAGE_AIR_STATE_OFF,
|
||||
ADVANTAGE_AIR_STATE_ON,
|
||||
ADVANTAGE_AIR_STATE_OPEN,
|
||||
DOMAIN as ADVANTAGE_AIR_DOMAIN,
|
||||
)
|
||||
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
|
||||
from .models import AdvantageAirData
|
||||
@@ -57,7 +58,7 @@ HVAC_MODES = [
|
||||
HVACMode.FAN_ONLY,
|
||||
HVACMode.DRY,
|
||||
]
|
||||
HVAC_MODES_MYAUTO = [*HVAC_MODES, HVACMode.HEAT_COOL]
|
||||
HVAC_MODES_MYAUTO = HVAC_MODES + [HVACMode.HEAT_COOL]
|
||||
SUPPORTED_FEATURES = (
|
||||
ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
@@ -75,12 +76,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir climate platform."""
|
||||
|
||||
instance = config_entry.runtime_data
|
||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
||||
|
||||
entities: list[ClimateEntity] = []
|
||||
if aircons := instance.coordinator.data.get("aircons"):
|
||||
|
@@ -45,7 +45,7 @@ class AdvantageAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
port=port,
|
||||
session=async_get_clientsession(self.hass),
|
||||
retry=ADVANTAGE_AIR_RETRY,
|
||||
).async_get()
|
||||
).async_get(1)
|
||||
except ApiError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
|
@@ -8,11 +8,15 @@ from homeassistant.components.cover import (
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import ADVANTAGE_AIR_STATE_CLOSE, ADVANTAGE_AIR_STATE_OPEN
|
||||
from .const import (
|
||||
ADVANTAGE_AIR_STATE_CLOSE,
|
||||
ADVANTAGE_AIR_STATE_OPEN,
|
||||
DOMAIN as ADVANTAGE_AIR_DOMAIN,
|
||||
)
|
||||
from .entity import AdvantageAirThingEntity, AdvantageAirZoneEntity
|
||||
from .models import AdvantageAirData
|
||||
|
||||
@@ -21,12 +25,12 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir cover platform."""
|
||||
|
||||
instance = config_entry.runtime_data
|
||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
||||
|
||||
entities: list[CoverEntity] = []
|
||||
if aircons := instance.coordinator.data.get("aircons"):
|
||||
|
@@ -5,9 +5,10 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
|
||||
TO_REDACT = [
|
||||
"dealerPhoneNumber",
|
||||
@@ -24,10 +25,10 @@ TO_REDACT = [
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: AdvantageAirDataConfigEntry
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""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 {
|
||||
|
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"services": {
|
||||
"set_time_to": "mdi:timer-cog"
|
||||
}
|
||||
}
|
@@ -3,11 +3,11 @@
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
from .entity import AdvantageAirEntity, AdvantageAirThingEntity
|
||||
from .models import AdvantageAirData
|
||||
@@ -15,12 +15,12 @@ from .models import AdvantageAirData
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir light platform."""
|
||||
|
||||
instance = config_entry.runtime_data
|
||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
||||
|
||||
entities: list[LightEntity] = []
|
||||
if my_lights := instance.coordinator.data.get("myLights"):
|
||||
|
@@ -1,10 +1,11 @@
|
||||
"""Select platform for Advantage Air integration."""
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
from .entity import AdvantageAirAcEntity
|
||||
from .models import AdvantageAirData
|
||||
|
||||
@@ -13,12 +14,12 @@ ADVANTAGE_AIR_INACTIVE = "Inactive"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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"):
|
||||
async_add_entities(AdvantageAirMyZone(instance, ac_key) for ac_key in aircons)
|
||||
|
@@ -12,13 +12,13 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import ADVANTAGE_AIR_STATE_OPEN
|
||||
from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
|
||||
from .models import AdvantageAirData
|
||||
|
||||
@@ -31,12 +31,12 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir sensor platform."""
|
||||
|
||||
instance = config_entry.runtime_data
|
||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
||||
|
||||
entities: list[SensorEntity] = []
|
||||
if aircons := instance.coordinator.data.get("aircons"):
|
||||
|
@@ -3,14 +3,15 @@
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import (
|
||||
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
||||
ADVANTAGE_AIR_STATE_OFF,
|
||||
ADVANTAGE_AIR_STATE_ON,
|
||||
DOMAIN as ADVANTAGE_AIR_DOMAIN,
|
||||
)
|
||||
from .entity import AdvantageAirAcEntity, AdvantageAirThingEntity
|
||||
from .models import AdvantageAirData
|
||||
@@ -18,12 +19,12 @@ from .models import AdvantageAirData
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir switch platform."""
|
||||
|
||||
instance = config_entry.runtime_data
|
||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
||||
|
||||
entities: list[SwitchEntity] = []
|
||||
if aircons := instance.coordinator.data.get("aircons"):
|
||||
|
@@ -1,11 +1,11 @@
|
||||
"""Advantage Air Update platform."""
|
||||
|
||||
from homeassistant.components.update import UpdateEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
from .entity import AdvantageAirEntity
|
||||
from .models import AdvantageAirData
|
||||
@@ -13,12 +13,12 @@ from .models import AdvantageAirData
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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)])
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
"""The AEMET OpenData component."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from aemet_opendata.exceptions import AemetError, TownNotFound
|
||||
@@ -12,23 +11,19 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type AemetConfigEntry = ConfigEntry[AemetData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AemetData:
|
||||
"""Aemet runtime data."""
|
||||
|
||||
name: str
|
||||
coordinator: WeatherUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up AEMET OpenData as config entry."""
|
||||
name = entry.data[CONF_NAME]
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -65,4 +64,9 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""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
|
||||
|
@@ -55,6 +55,8 @@ CONF_STATION_UPDATES = "station_updates"
|
||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
DEFAULT_NAME = "AEMET"
|
||||
DOMAIN = "aemet"
|
||||
ENTRY_NAME = "name"
|
||||
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
|
||||
|
||||
ATTR_API_CONDITION = "condition"
|
||||
ATTR_API_FORECAST_CONDITION = "condition"
|
||||
@@ -119,3 +121,8 @@ FORECAST_MAP = {
|
||||
AOD_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED,
|
||||
},
|
||||
}
|
||||
|
||||
WEATHER_FORECAST_MODES = {
|
||||
AOD_FORECAST_DAILY: "daily",
|
||||
AOD_FORECAST_HOURLY: "hourly",
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ from typing import Any
|
||||
from aemet_opendata.const import AOD_COORDS
|
||||
|
||||
from homeassistant.components.diagnostics.util import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LATITUDE,
|
||||
@@ -15,7 +16,8 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AemetConfigEntry
|
||||
from .const import DOMAIN, ENTRY_WEATHER_COORDINATOR
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
|
||||
TO_REDACT_CONFIG = [
|
||||
CONF_API_KEY,
|
||||
@@ -30,10 +32,11 @@ TO_REDACT_COORD = [
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: AemetConfigEntry
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""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 {
|
||||
"api_data": coordinator.aemet.raw_data(),
|
||||
|
@@ -56,7 +56,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import AemetConfigEntry
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_FORECAST_CONDITION,
|
||||
@@ -88,6 +87,9 @@ from .const import (
|
||||
ATTR_API_WIND_SPEED,
|
||||
ATTRIBUTION,
|
||||
CONDITIONS_MAP,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
)
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
from .entity import AemetEntity
|
||||
@@ -358,13 +360,13 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AemetConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AEMET OpenData sensor entities based on a config entry."""
|
||||
domain_data = config_entry.runtime_data
|
||||
name = domain_data.name
|
||||
coordinator = domain_data.coordinator
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
name: str = domain_data[ENTRY_NAME]
|
||||
coordinator: WeatherUpdateCoordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
|
||||
async_add_entities(
|
||||
AemetSensor(
|
||||
|
@@ -14,10 +14,12 @@ from aemet_opendata.const import (
|
||||
)
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
Forecast,
|
||||
SingleCoordinatorWeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfPressure,
|
||||
@@ -25,28 +27,55 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AemetConfigEntry
|
||||
from .const import ATTRIBUTION, CONDITIONS_MAP
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
CONDITIONS_MAP,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
WEATHER_FORECAST_MODES,
|
||||
)
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
from .entity import AemetEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AemetConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AEMET OpenData weather entity based on a config entry."""
|
||||
domain_data = config_entry.runtime_data
|
||||
name = domain_data.name
|
||||
weather_coordinator = domain_data.coordinator
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
|
||||
async_add_entities(
|
||||
[AemetWeather(name, config_entry.unique_id, weather_coordinator)],
|
||||
False,
|
||||
)
|
||||
entities = []
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
# 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(
|
||||
@@ -69,9 +98,14 @@ class AemetWeather(
|
||||
name,
|
||||
unique_id,
|
||||
coordinator: WeatherUpdateCoordinator,
|
||||
forecast_mode,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
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_unique_id = unique_id
|
||||
|
||||
@@ -81,6 +115,11 @@ class AemetWeather(
|
||||
cond = self.get_aemet_value([AOD_WEATHER, AOD_CONDITION])
|
||||
return CONDITIONS_MAP.get(cond)
|
||||
|
||||
@property
|
||||
def forecast(self) -> list[Forecast]:
|
||||
"""Return the forecast array."""
|
||||
return self.get_aemet_forecast(self._forecast_mode)
|
||||
|
||||
@callback
|
||||
def _async_forecast_daily(self) -> list[Forecast]:
|
||||
"""Return the daily forecast in native units."""
|
||||
|
@@ -10,14 +10,16 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
type AfterShipConfigEntry = ConfigEntry[AfterShip]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AfterShipConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up AfterShip from a config entry."""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
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:
|
||||
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)
|
||||
|
||||
@@ -35,4 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AfterShipConfigEntry) ->
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""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
|
||||
|
@@ -8,6 +8,7 @@ from typing import Any, Final
|
||||
from pyaftership import AfterShip, AfterShipException
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
@@ -17,7 +18,6 @@ from homeassistant.helpers.dispatcher import (
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from . import AfterShipConfigEntry
|
||||
from .const import (
|
||||
ADD_TRACKING_SERVICE_SCHEMA,
|
||||
ATTR_TRACKINGS,
|
||||
@@ -41,11 +41,11 @@ PLATFORM_SCHEMA: Final = cv.removed(DOMAIN, raise_if_present=False)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AfterShipConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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)
|
||||
|
||||
|
@@ -10,20 +10,18 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
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"
|
||||
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
|
||||
|
||||
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
|
||||
|
||||
AgentDVRConfigEntry = ConfigEntry[Agent]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: AgentDVRConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up the Agent component."""
|
||||
hass.data.setdefault(AGENT_DOMAIN, {})
|
||||
|
||||
server_origin = config_entry.data[SERVER_URL]
|
||||
|
||||
agent_client = Agent(server_origin, async_get_clientsession(hass))
|
||||
@@ -36,11 +34,9 @@ async def async_setup_entry(
|
||||
if not agent_client.is_available:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
config_entry.async_on_unload(agent_client.close)
|
||||
|
||||
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)
|
||||
|
||||
@@ -58,8 +54,15 @@ async def async_setup_entry(
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: AgentDVRConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""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
|
||||
|
@@ -6,6 +6,7 @@ from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
AlarmControlPanelEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
@@ -16,8 +17,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AgentDVRConfigEntry
|
||||
from .const import DOMAIN as AGENT_DOMAIN
|
||||
from .const import CONNECTION, DOMAIN as AGENT_DOMAIN
|
||||
|
||||
CONF_HOME_MODE_NAME = "home"
|
||||
CONF_AWAY_MODE_NAME = "away"
|
||||
@@ -28,11 +28,13 @@ CONST_ALARM_CONTROL_PANEL_NAME = "Alarm Panel"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AgentDVRConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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):
|
||||
|
@@ -7,6 +7,7 @@ from agent import AgentError
|
||||
|
||||
from homeassistant.components.camera import CameraEntityFeature
|
||||
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
@@ -14,8 +15,12 @@ from homeassistant.helpers.entity_platform import (
|
||||
async_get_current_platform,
|
||||
)
|
||||
|
||||
from . import AgentDVRConfigEntry
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
CAMERA_SCAN_INTERVAL_SECS,
|
||||
CONNECTION,
|
||||
DOMAIN as AGENT_DOMAIN,
|
||||
)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
|
||||
|
||||
@@ -38,14 +43,14 @@ CAMERA_SERVICES = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AgentDVRConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Agent cameras."""
|
||||
filter_urllib3_logging()
|
||||
cameras = []
|
||||
|
||||
server = config_entry.runtime_data
|
||||
server = hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION]
|
||||
if not server.devices:
|
||||
_LOGGER.warning("Could not fetch cameras from Agent server")
|
||||
return
|
||||
@@ -75,11 +80,11 @@ class AgentCamera(MjpegCamera):
|
||||
"""Initialize as a subclass of MjpegCamera."""
|
||||
self.device = device
|
||||
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__(
|
||||
name=device.name,
|
||||
mjpeg_url=f"{device.client._server_url}{device.mjpeg_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}", # 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}",
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(AGENT_DOMAIN, self.unique_id)},
|
||||
|
@@ -9,3 +9,4 @@ SERVICE_UPDATE = "update"
|
||||
SIGNAL_UPDATE_AGENT = "agent_update"
|
||||
ATTRIBUTION = "Data provided by ispyconnect.com"
|
||||
SERVER_URL = "server_url"
|
||||
CONNECTION = "connection"
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
@@ -17,9 +17,6 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
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__)
|
||||
|
||||
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_SO2: Final = "sulphur_dioxide"
|
||||
|
||||
DOMAIN: Final = "air_quality"
|
||||
|
||||
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
||||
|
||||
SCAN_INTERVAL: Final = timedelta(seconds=30)
|
||||
@@ -82,7 +81,7 @@ class AirQualityEntity(Entity):
|
||||
@property
|
||||
def particulate_matter_2_5(self) -> StateType:
|
||||
"""Return the particulate matter 2.5 level."""
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def particulate_matter_10(self) -> StateType:
|
||||
|
@@ -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
Reference in New Issue
Block a user