This commit is contained in:
Franck Nijhof 2024-06-05 20:06:01 +02:00 committed by GitHub
commit 460909a7f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3566 changed files with 112663 additions and 47455 deletions

View File

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

View File

@ -58,13 +58,18 @@ omit =
homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual/sensor.py
homeassistant/components/airvisual_pro/__init__.py homeassistant/components/airvisual_pro/__init__.py
homeassistant/components/airvisual_pro/sensor.py homeassistant/components/airvisual_pro/sensor.py
homeassistant/components/aladdin_connect/__init__.py
homeassistant/components/aladdin_connect/api.py
homeassistant/components/aladdin_connect/application_credentials.py
homeassistant/components/aladdin_connect/cover.py
homeassistant/components/aladdin_connect/sensor.py
homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/__init__.py
homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/alarm_control_panel.py
homeassistant/components/alarmdecoder/binary_sensor.py homeassistant/components/alarmdecoder/binary_sensor.py
homeassistant/components/alarmdecoder/entity.py
homeassistant/components/alarmdecoder/sensor.py homeassistant/components/alarmdecoder/sensor.py
homeassistant/components/alpha_vantage/sensor.py homeassistant/components/alpha_vantage/sensor.py
homeassistant/components/amazon_polly/* homeassistant/components/amazon_polly/*
homeassistant/components/ambiclimate/climate.py
homeassistant/components/ambient_station/__init__.py homeassistant/components/ambient_station/__init__.py
homeassistant/components/ambient_station/binary_sensor.py homeassistant/components/ambient_station/binary_sensor.py
homeassistant/components/ambient_station/entity.py homeassistant/components/ambient_station/entity.py
@ -82,6 +87,9 @@ omit =
homeassistant/components/aprilaire/climate.py homeassistant/components/aprilaire/climate.py
homeassistant/components/aprilaire/coordinator.py homeassistant/components/aprilaire/coordinator.py
homeassistant/components/aprilaire/entity.py homeassistant/components/aprilaire/entity.py
homeassistant/components/apsystems/__init__.py
homeassistant/components/apsystems/coordinator.py
homeassistant/components/apsystems/sensor.py
homeassistant/components/aqualogic/* homeassistant/components/aqualogic/*
homeassistant/components/aquostv/media_player.py homeassistant/components/aquostv/media_player.py
homeassistant/components/arcam_fmj/__init__.py homeassistant/components/arcam_fmj/__init__.py
@ -120,7 +128,6 @@ omit =
homeassistant/components/baf/switch.py homeassistant/components/baf/switch.py
homeassistant/components/baidu/tts.py homeassistant/components/baidu/tts.py
homeassistant/components/bang_olufsen/__init__.py homeassistant/components/bang_olufsen/__init__.py
homeassistant/components/bang_olufsen/const.py
homeassistant/components/bang_olufsen/entity.py homeassistant/components/bang_olufsen/entity.py
homeassistant/components/bang_olufsen/media_player.py homeassistant/components/bang_olufsen/media_player.py
homeassistant/components/bang_olufsen/util.py homeassistant/components/bang_olufsen/util.py
@ -192,7 +199,6 @@ omit =
homeassistant/components/comelit/__init__.py homeassistant/components/comelit/__init__.py
homeassistant/components/comelit/alarm_control_panel.py homeassistant/components/comelit/alarm_control_panel.py
homeassistant/components/comelit/climate.py homeassistant/components/comelit/climate.py
homeassistant/components/comelit/const.py
homeassistant/components/comelit/coordinator.py homeassistant/components/comelit/coordinator.py
homeassistant/components/comelit/cover.py homeassistant/components/comelit/cover.py
homeassistant/components/comelit/humidifier.py homeassistant/components/comelit/humidifier.py
@ -255,9 +261,6 @@ omit =
homeassistant/components/dormakaba_dkey/sensor.py homeassistant/components/dormakaba_dkey/sensor.py
homeassistant/components/dovado/* homeassistant/components/dovado/*
homeassistant/components/downloader/__init__.py homeassistant/components/downloader/__init__.py
homeassistant/components/dsmr_reader/__init__.py
homeassistant/components/dsmr_reader/definitions.py
homeassistant/components/dsmr_reader/sensor.py
homeassistant/components/dte_energy_bridge/sensor.py homeassistant/components/dte_energy_bridge/sensor.py
homeassistant/components/dublin_bus_transport/sensor.py homeassistant/components/dublin_bus_transport/sensor.py
homeassistant/components/dunehd/__init__.py homeassistant/components/dunehd/__init__.py
@ -269,7 +272,6 @@ omit =
homeassistant/components/duotecno/entity.py homeassistant/components/duotecno/entity.py
homeassistant/components/duotecno/light.py homeassistant/components/duotecno/light.py
homeassistant/components/duotecno/switch.py homeassistant/components/duotecno/switch.py
homeassistant/components/dwd_weather_warnings/const.py
homeassistant/components/dwd_weather_warnings/coordinator.py homeassistant/components/dwd_weather_warnings/coordinator.py
homeassistant/components/dwd_weather_warnings/sensor.py homeassistant/components/dwd_weather_warnings/sensor.py
homeassistant/components/dweet/* homeassistant/components/dweet/*
@ -326,8 +328,7 @@ omit =
homeassistant/components/elmax/__init__.py homeassistant/components/elmax/__init__.py
homeassistant/components/elmax/alarm_control_panel.py homeassistant/components/elmax/alarm_control_panel.py
homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/binary_sensor.py
homeassistant/components/elmax/common.py homeassistant/components/elmax/coordinator.py
homeassistant/components/elmax/const.py
homeassistant/components/elmax/cover.py homeassistant/components/elmax/cover.py
homeassistant/components/elmax/switch.py homeassistant/components/elmax/switch.py
homeassistant/components/elv/* homeassistant/components/elv/*
@ -370,7 +371,6 @@ omit =
homeassistant/components/epson/media_player.py homeassistant/components/epson/media_player.py
homeassistant/components/eq3btsmart/__init__.py homeassistant/components/eq3btsmart/__init__.py
homeassistant/components/eq3btsmart/climate.py homeassistant/components/eq3btsmart/climate.py
homeassistant/components/eq3btsmart/const.py
homeassistant/components/eq3btsmart/entity.py homeassistant/components/eq3btsmart/entity.py
homeassistant/components/eq3btsmart/models.py homeassistant/components/eq3btsmart/models.py
homeassistant/components/escea/__init__.py homeassistant/components/escea/__init__.py
@ -462,8 +462,8 @@ omit =
homeassistant/components/freebox/camera.py homeassistant/components/freebox/camera.py
homeassistant/components/freebox/home_base.py homeassistant/components/freebox/home_base.py
homeassistant/components/freebox/switch.py homeassistant/components/freebox/switch.py
homeassistant/components/fritz/common.py homeassistant/components/fritz/coordinator.py
homeassistant/components/fritz/device_tracker.py homeassistant/components/fritz/entity.py
homeassistant/components/fritz/services.py homeassistant/components/fritz/services.py
homeassistant/components/fritz/switch.py homeassistant/components/fritz/switch.py
homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/__init__.py
@ -473,10 +473,6 @@ omit =
homeassistant/components/frontier_silicon/browse_media.py homeassistant/components/frontier_silicon/browse_media.py
homeassistant/components/frontier_silicon/media_player.py homeassistant/components/frontier_silicon/media_player.py
homeassistant/components/futurenow/light.py homeassistant/components/futurenow/light.py
homeassistant/components/fyta/__init__.py
homeassistant/components/fyta/coordinator.py
homeassistant/components/fyta/entity.py
homeassistant/components/fyta/sensor.py
homeassistant/components/garadget/cover.py homeassistant/components/garadget/cover.py
homeassistant/components/garages_amsterdam/__init__.py homeassistant/components/garages_amsterdam/__init__.py
homeassistant/components/garages_amsterdam/binary_sensor.py homeassistant/components/garages_amsterdam/binary_sensor.py
@ -505,7 +501,6 @@ omit =
homeassistant/components/gpsd/sensor.py homeassistant/components/gpsd/sensor.py
homeassistant/components/greenwave/light.py homeassistant/components/greenwave/light.py
homeassistant/components/growatt_server/__init__.py homeassistant/components/growatt_server/__init__.py
homeassistant/components/growatt_server/const.py
homeassistant/components/growatt_server/sensor.py homeassistant/components/growatt_server/sensor.py
homeassistant/components/growatt_server/sensor_types/* homeassistant/components/growatt_server/sensor_types/*
homeassistant/components/gstreamer/media_player.py homeassistant/components/gstreamer/media_player.py
@ -519,6 +514,7 @@ omit =
homeassistant/components/guardian/util.py homeassistant/components/guardian/util.py
homeassistant/components/guardian/valve.py homeassistant/components/guardian/valve.py
homeassistant/components/habitica/__init__.py homeassistant/components/habitica/__init__.py
homeassistant/components/habitica/coordinator.py
homeassistant/components/habitica/sensor.py homeassistant/components/habitica/sensor.py
homeassistant/components/harman_kardon_avr/media_player.py homeassistant/components/harman_kardon_avr/media_player.py
homeassistant/components/harmony/data.py homeassistant/components/harmony/data.py
@ -684,6 +680,7 @@ omit =
homeassistant/components/konnected/panel.py homeassistant/components/konnected/panel.py
homeassistant/components/konnected/switch.py homeassistant/components/konnected/switch.py
homeassistant/components/kostal_plenticore/__init__.py homeassistant/components/kostal_plenticore/__init__.py
homeassistant/components/kostal_plenticore/coordinator.py
homeassistant/components/kostal_plenticore/helper.py homeassistant/components/kostal_plenticore/helper.py
homeassistant/components/kostal_plenticore/select.py homeassistant/components/kostal_plenticore/select.py
homeassistant/components/kostal_plenticore/sensor.py homeassistant/components/kostal_plenticore/sensor.py
@ -731,7 +728,6 @@ omit =
homeassistant/components/lookin/sensor.py homeassistant/components/lookin/sensor.py
homeassistant/components/loqed/sensor.py homeassistant/components/loqed/sensor.py
homeassistant/components/luci/device_tracker.py homeassistant/components/luci/device_tracker.py
homeassistant/components/luftdaten/sensor.py
homeassistant/components/lupusec/__init__.py homeassistant/components/lupusec/__init__.py
homeassistant/components/lupusec/alarm_control_panel.py homeassistant/components/lupusec/alarm_control_panel.py
homeassistant/components/lupusec/binary_sensor.py homeassistant/components/lupusec/binary_sensor.py
@ -761,6 +757,7 @@ omit =
homeassistant/components/matrix/__init__.py homeassistant/components/matrix/__init__.py
homeassistant/components/matrix/notify.py homeassistant/components/matrix/notify.py
homeassistant/components/matter/__init__.py homeassistant/components/matter/__init__.py
homeassistant/components/matter/fan.py
homeassistant/components/meater/__init__.py homeassistant/components/meater/__init__.py
homeassistant/components/meater/sensor.py homeassistant/components/meater/sensor.py
homeassistant/components/medcom_ble/__init__.py homeassistant/components/medcom_ble/__init__.py
@ -787,7 +784,7 @@ omit =
homeassistant/components/microbees/application_credentials.py homeassistant/components/microbees/application_credentials.py
homeassistant/components/microbees/binary_sensor.py homeassistant/components/microbees/binary_sensor.py
homeassistant/components/microbees/button.py homeassistant/components/microbees/button.py
homeassistant/components/microbees/const.py homeassistant/components/microbees/climate.py
homeassistant/components/microbees/coordinator.py homeassistant/components/microbees/coordinator.py
homeassistant/components/microbees/cover.py homeassistant/components/microbees/cover.py
homeassistant/components/microbees/entity.py homeassistant/components/microbees/entity.py
@ -795,7 +792,7 @@ omit =
homeassistant/components/microbees/sensor.py homeassistant/components/microbees/sensor.py
homeassistant/components/microbees/switch.py homeassistant/components/microbees/switch.py
homeassistant/components/microsoft/tts.py homeassistant/components/microsoft/tts.py
homeassistant/components/mikrotik/hub.py homeassistant/components/mikrotik/coordinator.py
homeassistant/components/mill/climate.py homeassistant/components/mill/climate.py
homeassistant/components/mill/sensor.py homeassistant/components/mill/sensor.py
homeassistant/components/minio/minio_helper.py homeassistant/components/minio/minio_helper.py
@ -806,10 +803,10 @@ omit =
homeassistant/components/mochad/switch.py homeassistant/components/mochad/switch.py
homeassistant/components/modem_callerid/button.py homeassistant/components/modem_callerid/button.py
homeassistant/components/modem_callerid/sensor.py homeassistant/components/modem_callerid/sensor.py
homeassistant/components/moehlenhoff_alpha2/__init__.py
homeassistant/components/moehlenhoff_alpha2/binary_sensor.py
homeassistant/components/moehlenhoff_alpha2/climate.py homeassistant/components/moehlenhoff_alpha2/climate.py
homeassistant/components/moehlenhoff_alpha2/sensor.py homeassistant/components/moehlenhoff_alpha2/coordinator.py
homeassistant/components/monzo/__init__.py
homeassistant/components/monzo/api.py
homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/__init__.py
homeassistant/components/motion_blinds/coordinator.py homeassistant/components/motion_blinds/coordinator.py
homeassistant/components/motion_blinds/cover.py homeassistant/components/motion_blinds/cover.py
@ -919,9 +916,8 @@ omit =
homeassistant/components/notion/util.py homeassistant/components/notion/util.py
homeassistant/components/nsw_fuel_station/sensor.py homeassistant/components/nsw_fuel_station/sensor.py
homeassistant/components/nuki/__init__.py homeassistant/components/nuki/__init__.py
homeassistant/components/nuki/binary_sensor.py homeassistant/components/nuki/coordinator.py
homeassistant/components/nuki/lock.py homeassistant/components/nuki/lock.py
homeassistant/components/nuki/sensor.py
homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nx584/alarm_control_panel.py
homeassistant/components/oasa_telematics/sensor.py homeassistant/components/oasa_telematics/sensor.py
homeassistant/components/obihai/__init__.py homeassistant/components/obihai/__init__.py
@ -934,7 +930,7 @@ omit =
homeassistant/components/ohmconnect/sensor.py homeassistant/components/ohmconnect/sensor.py
homeassistant/components/ombi/* homeassistant/components/ombi/*
homeassistant/components/omnilogic/__init__.py homeassistant/components/omnilogic/__init__.py
homeassistant/components/omnilogic/common.py homeassistant/components/omnilogic/coordinator.py
homeassistant/components/omnilogic/sensor.py homeassistant/components/omnilogic/sensor.py
homeassistant/components/omnilogic/switch.py homeassistant/components/omnilogic/switch.py
homeassistant/components/ondilo_ico/__init__.py homeassistant/components/ondilo_ico/__init__.py
@ -962,7 +958,6 @@ omit =
homeassistant/components/opengarage/sensor.py homeassistant/components/opengarage/sensor.py
homeassistant/components/openhardwaremonitor/sensor.py homeassistant/components/openhardwaremonitor/sensor.py
homeassistant/components/openhome/__init__.py homeassistant/components/openhome/__init__.py
homeassistant/components/openhome/const.py
homeassistant/components/openhome/media_player.py homeassistant/components/openhome/media_player.py
homeassistant/components/opensensemap/air_quality.py homeassistant/components/opensensemap/air_quality.py
homeassistant/components/opentherm_gw/__init__.py homeassistant/components/opentherm_gw/__init__.py
@ -974,9 +969,10 @@ omit =
homeassistant/components/openuv/coordinator.py homeassistant/components/openuv/coordinator.py
homeassistant/components/openuv/sensor.py homeassistant/components/openuv/sensor.py
homeassistant/components/openweathermap/__init__.py homeassistant/components/openweathermap/__init__.py
homeassistant/components/openweathermap/coordinator.py
homeassistant/components/openweathermap/repairs.py
homeassistant/components/openweathermap/sensor.py homeassistant/components/openweathermap/sensor.py
homeassistant/components/openweathermap/weather.py homeassistant/components/openweathermap/weather.py
homeassistant/components/openweathermap/weather_update_coordinator.py
homeassistant/components/opnsense/__init__.py homeassistant/components/opnsense/__init__.py
homeassistant/components/opnsense/device_tracker.py homeassistant/components/opnsense/device_tracker.py
homeassistant/components/opower/__init__.py homeassistant/components/opower/__init__.py
@ -986,7 +982,7 @@ omit =
homeassistant/components/oru/* homeassistant/components/oru/*
homeassistant/components/orvibo/switch.py homeassistant/components/orvibo/switch.py
homeassistant/components/osoenergy/__init__.py homeassistant/components/osoenergy/__init__.py
homeassistant/components/osoenergy/const.py homeassistant/components/osoenergy/binary_sensor.py
homeassistant/components/osoenergy/sensor.py homeassistant/components/osoenergy/sensor.py
homeassistant/components/osoenergy/water_heater.py homeassistant/components/osoenergy/water_heater.py
homeassistant/components/osramlightify/light.py homeassistant/components/osramlightify/light.py
@ -1023,6 +1019,7 @@ omit =
homeassistant/components/permobil/entity.py homeassistant/components/permobil/entity.py
homeassistant/components/permobil/sensor.py homeassistant/components/permobil/sensor.py
homeassistant/components/philips_js/__init__.py homeassistant/components/philips_js/__init__.py
homeassistant/components/philips_js/coordinator.py
homeassistant/components/philips_js/light.py homeassistant/components/philips_js/light.py
homeassistant/components/philips_js/media_player.py homeassistant/components/philips_js/media_player.py
homeassistant/components/philips_js/remote.py homeassistant/components/philips_js/remote.py
@ -1031,7 +1028,6 @@ omit =
homeassistant/components/picotts/tts.py homeassistant/components/picotts/tts.py
homeassistant/components/pilight/base_class.py homeassistant/components/pilight/base_class.py
homeassistant/components/pilight/binary_sensor.py homeassistant/components/pilight/binary_sensor.py
homeassistant/components/pilight/const.py
homeassistant/components/pilight/light.py homeassistant/components/pilight/light.py
homeassistant/components/pilight/switch.py homeassistant/components/pilight/switch.py
homeassistant/components/ping/__init__.py homeassistant/components/ping/__init__.py
@ -1050,11 +1046,6 @@ omit =
homeassistant/components/point/alarm_control_panel.py homeassistant/components/point/alarm_control_panel.py
homeassistant/components/point/binary_sensor.py homeassistant/components/point/binary_sensor.py
homeassistant/components/point/sensor.py homeassistant/components/point/sensor.py
homeassistant/components/poolsense/__init__.py
homeassistant/components/poolsense/binary_sensor.py
homeassistant/components/poolsense/coordinator.py
homeassistant/components/poolsense/entity.py
homeassistant/components/poolsense/sensor.py
homeassistant/components/powerwall/__init__.py homeassistant/components/powerwall/__init__.py
homeassistant/components/progettihwsw/__init__.py homeassistant/components/progettihwsw/__init__.py
homeassistant/components/progettihwsw/binary_sensor.py homeassistant/components/progettihwsw/binary_sensor.py
@ -1081,7 +1072,6 @@ omit =
homeassistant/components/quantum_gateway/device_tracker.py homeassistant/components/quantum_gateway/device_tracker.py
homeassistant/components/qvr_pro/* homeassistant/components/qvr_pro/*
homeassistant/components/rabbitair/__init__.py homeassistant/components/rabbitair/__init__.py
homeassistant/components/rabbitair/const.py
homeassistant/components/rabbitair/coordinator.py homeassistant/components/rabbitair/coordinator.py
homeassistant/components/rabbitair/entity.py homeassistant/components/rabbitair/entity.py
homeassistant/components/rabbitair/fan.py homeassistant/components/rabbitair/fan.py
@ -1104,6 +1094,7 @@ omit =
homeassistant/components/rainmachine/__init__.py homeassistant/components/rainmachine/__init__.py
homeassistant/components/rainmachine/binary_sensor.py homeassistant/components/rainmachine/binary_sensor.py
homeassistant/components/rainmachine/button.py homeassistant/components/rainmachine/button.py
homeassistant/components/rainmachine/coordinator.py
homeassistant/components/rainmachine/select.py homeassistant/components/rainmachine/select.py
homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/sensor.py
homeassistant/components/rainmachine/switch.py homeassistant/components/rainmachine/switch.py
@ -1126,7 +1117,6 @@ omit =
homeassistant/components/renson/__init__.py homeassistant/components/renson/__init__.py
homeassistant/components/renson/binary_sensor.py homeassistant/components/renson/binary_sensor.py
homeassistant/components/renson/button.py homeassistant/components/renson/button.py
homeassistant/components/renson/const.py
homeassistant/components/renson/coordinator.py homeassistant/components/renson/coordinator.py
homeassistant/components/renson/entity.py homeassistant/components/renson/entity.py
homeassistant/components/renson/fan.py homeassistant/components/renson/fan.py
@ -1195,13 +1185,11 @@ omit =
homeassistant/components/schluter/* homeassistant/components/schluter/*
homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/binary_sensor.py
homeassistant/components/screenlogic/climate.py homeassistant/components/screenlogic/climate.py
homeassistant/components/screenlogic/const.py
homeassistant/components/screenlogic/coordinator.py homeassistant/components/screenlogic/coordinator.py
homeassistant/components/screenlogic/entity.py homeassistant/components/screenlogic/entity.py
homeassistant/components/screenlogic/light.py homeassistant/components/screenlogic/light.py
homeassistant/components/screenlogic/number.py homeassistant/components/screenlogic/number.py
homeassistant/components/screenlogic/sensor.py homeassistant/components/screenlogic/sensor.py
homeassistant/components/screenlogic/services.py
homeassistant/components/screenlogic/switch.py homeassistant/components/screenlogic/switch.py
homeassistant/components/scsgate/* homeassistant/components/scsgate/*
homeassistant/components/sendgrid/notify.py homeassistant/components/sendgrid/notify.py
@ -1254,7 +1242,6 @@ omit =
homeassistant/components/smappee/switch.py homeassistant/components/smappee/switch.py
homeassistant/components/smarty/* homeassistant/components/smarty/*
homeassistant/components/sms/__init__.py homeassistant/components/sms/__init__.py
homeassistant/components/sms/const.py
homeassistant/components/sms/coordinator.py homeassistant/components/sms/coordinator.py
homeassistant/components/sms/gateway.py homeassistant/components/sms/gateway.py
homeassistant/components/sms/notify.py homeassistant/components/sms/notify.py
@ -1348,6 +1335,7 @@ omit =
homeassistant/components/supla/* homeassistant/components/supla/*
homeassistant/components/surepetcare/__init__.py homeassistant/components/surepetcare/__init__.py
homeassistant/components/surepetcare/binary_sensor.py homeassistant/components/surepetcare/binary_sensor.py
homeassistant/components/surepetcare/coordinator.py
homeassistant/components/surepetcare/entity.py homeassistant/components/surepetcare/entity.py
homeassistant/components/surepetcare/sensor.py homeassistant/components/surepetcare/sensor.py
homeassistant/components/swiss_hydrological_data/sensor.py homeassistant/components/swiss_hydrological_data/sensor.py
@ -1376,6 +1364,7 @@ omit =
homeassistant/components/switchbot_cloud/climate.py homeassistant/components/switchbot_cloud/climate.py
homeassistant/components/switchbot_cloud/coordinator.py homeassistant/components/switchbot_cloud/coordinator.py
homeassistant/components/switchbot_cloud/entity.py homeassistant/components/switchbot_cloud/entity.py
homeassistant/components/switchbot_cloud/sensor.py
homeassistant/components/switchbot_cloud/switch.py homeassistant/components/switchbot_cloud/switch.py
homeassistant/components/switchmate/switch.py homeassistant/components/switchmate/switch.py
homeassistant/components/syncthing/__init__.py homeassistant/components/syncthing/__init__.py
@ -1434,12 +1423,11 @@ omit =
homeassistant/components/tensorflow/image_processing.py homeassistant/components/tensorflow/image_processing.py
homeassistant/components/tfiac/climate.py homeassistant/components/tfiac/climate.py
homeassistant/components/thermoworks_smoke/sensor.py homeassistant/components/thermoworks_smoke/sensor.py
homeassistant/components/thethingsnetwork/*
homeassistant/components/thingspeak/* homeassistant/components/thingspeak/*
homeassistant/components/thinkingcleaner/* homeassistant/components/thinkingcleaner/*
homeassistant/components/thomson/device_tracker.py homeassistant/components/thomson/device_tracker.py
homeassistant/components/tibber/__init__.py homeassistant/components/tibber/__init__.py
homeassistant/components/tibber/notify.py homeassistant/components/tibber/coordinator.py
homeassistant/components/tibber/sensor.py homeassistant/components/tibber/sensor.py
homeassistant/components/tikteck/light.py homeassistant/components/tikteck/light.py
homeassistant/components/tile/__init__.py homeassistant/components/tile/__init__.py
@ -1545,8 +1533,9 @@ omit =
homeassistant/components/v2c/coordinator.py homeassistant/components/v2c/coordinator.py
homeassistant/components/v2c/entity.py homeassistant/components/v2c/entity.py
homeassistant/components/v2c/number.py homeassistant/components/v2c/number.py
homeassistant/components/v2c/sensor.py
homeassistant/components/v2c/switch.py homeassistant/components/v2c/switch.py
homeassistant/components/vallox/__init__.py
homeassistant/components/vallox/coordinator.py
homeassistant/components/vasttrafik/sensor.py homeassistant/components/vasttrafik/sensor.py
homeassistant/components/velbus/__init__.py homeassistant/components/velbus/__init__.py
homeassistant/components/velbus/binary_sensor.py homeassistant/components/velbus/binary_sensor.py
@ -1561,9 +1550,8 @@ omit =
homeassistant/components/velux/__init__.py homeassistant/components/velux/__init__.py
homeassistant/components/velux/cover.py homeassistant/components/velux/cover.py
homeassistant/components/velux/light.py homeassistant/components/velux/light.py
homeassistant/components/venstar/__init__.py
homeassistant/components/venstar/binary_sensor.py
homeassistant/components/venstar/climate.py homeassistant/components/venstar/climate.py
homeassistant/components/venstar/coordinator.py
homeassistant/components/venstar/sensor.py homeassistant/components/venstar/sensor.py
homeassistant/components/verisure/__init__.py homeassistant/components/verisure/__init__.py
homeassistant/components/verisure/alarm_control_panel.py homeassistant/components/verisure/alarm_control_panel.py
@ -1596,7 +1584,6 @@ omit =
homeassistant/components/vlc_telnet/media_player.py homeassistant/components/vlc_telnet/media_player.py
homeassistant/components/vodafone_station/__init__.py homeassistant/components/vodafone_station/__init__.py
homeassistant/components/vodafone_station/button.py homeassistant/components/vodafone_station/button.py
homeassistant/components/vodafone_station/const.py
homeassistant/components/vodafone_station/coordinator.py homeassistant/components/vodafone_station/coordinator.py
homeassistant/components/vodafone_station/device_tracker.py homeassistant/components/vodafone_station/device_tracker.py
homeassistant/components/vodafone_station/sensor.py homeassistant/components/vodafone_station/sensor.py
@ -1621,10 +1608,8 @@ omit =
homeassistant/components/watttime/__init__.py homeassistant/components/watttime/__init__.py
homeassistant/components/watttime/sensor.py homeassistant/components/watttime/sensor.py
homeassistant/components/weatherflow/__init__.py homeassistant/components/weatherflow/__init__.py
homeassistant/components/weatherflow/const.py
homeassistant/components/weatherflow/sensor.py homeassistant/components/weatherflow/sensor.py
homeassistant/components/weatherflow_cloud/__init__.py homeassistant/components/weatherflow_cloud/__init__.py
homeassistant/components/weatherflow_cloud/const.py
homeassistant/components/weatherflow_cloud/coordinator.py homeassistant/components/weatherflow_cloud/coordinator.py
homeassistant/components/weatherflow_cloud/weather.py homeassistant/components/weatherflow_cloud/weather.py
homeassistant/components/wiffi/__init__.py homeassistant/components/wiffi/__init__.py
@ -1642,6 +1627,7 @@ omit =
homeassistant/components/xbox/base_sensor.py homeassistant/components/xbox/base_sensor.py
homeassistant/components/xbox/binary_sensor.py homeassistant/components/xbox/binary_sensor.py
homeassistant/components/xbox/browse_media.py homeassistant/components/xbox/browse_media.py
homeassistant/components/xbox/coordinator.py
homeassistant/components/xbox/media_player.py homeassistant/components/xbox/media_player.py
homeassistant/components/xbox/remote.py homeassistant/components/xbox/remote.py
homeassistant/components/xbox/sensor.py homeassistant/components/xbox/sensor.py
@ -1674,10 +1660,7 @@ omit =
homeassistant/components/xs1/* homeassistant/components/xs1/*
homeassistant/components/yale_smart_alarm/__init__.py homeassistant/components/yale_smart_alarm/__init__.py
homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yale_smart_alarm/alarm_control_panel.py
homeassistant/components/yale_smart_alarm/binary_sensor.py
homeassistant/components/yale_smart_alarm/button.py
homeassistant/components/yale_smart_alarm/entity.py homeassistant/components/yale_smart_alarm/entity.py
homeassistant/components/yale_smart_alarm/lock.py
homeassistant/components/yalexs_ble/__init__.py homeassistant/components/yalexs_ble/__init__.py
homeassistant/components/yalexs_ble/binary_sensor.py homeassistant/components/yalexs_ble/binary_sensor.py
homeassistant/components/yalexs_ble/entity.py homeassistant/components/yalexs_ble/entity.py
@ -1718,10 +1701,6 @@ omit =
homeassistant/components/zeroconf/models.py homeassistant/components/zeroconf/models.py
homeassistant/components/zeroconf/usage.py homeassistant/components/zeroconf/usage.py
homeassistant/components/zestimate/sensor.py homeassistant/components/zestimate/sensor.py
homeassistant/components/zeversolar/__init__.py
homeassistant/components/zeversolar/coordinator.py
homeassistant/components/zeversolar/entity.py
homeassistant/components/zeversolar/sensor.py
homeassistant/components/zha/core/cluster_handlers/* homeassistant/components/zha/core/cluster_handlers/*
homeassistant/components/zha/core/device.py homeassistant/components/zha/core/device.py
homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/gateway.py

View File

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

View File

@ -27,7 +27,7 @@ jobs:
publish: ${{ steps.version.outputs.publish }} publish: ${{ steps.version.outputs.publish }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
with: with:
fetch-depth: 0 fetch-depth: 0
@ -90,7 +90,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Download nightly wheels of frontend - name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
@ -175,7 +175,7 @@ jobs:
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations - name: Download translations
uses: actions/download-artifact@v4.1.6 uses: actions/download-artifact@v4.1.7
with: with:
name: translations name: translations
@ -190,7 +190,7 @@ jobs:
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3.1.0 uses: docker/login-action@v3.2.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -242,7 +242,7 @@ jobs:
- green - green
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set build additional args - name: Set build additional args
run: | run: |
@ -256,7 +256,7 @@ jobs:
fi fi
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3.1.0 uses: docker/login-action@v3.2.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -279,7 +279,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Initialize git - name: Initialize git
uses: home-assistant/actions/helpers/git-init@master uses: home-assistant/actions/helpers/git-init@master
@ -320,23 +320,23 @@ jobs:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"] registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@v3.4.0 uses: sigstore/cosign-installer@v3.5.0
with: with:
cosign-release: "v2.2.3" cosign-release: "v2.2.3"
- name: Login to DockerHub - name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant' if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@v3.1.0 uses: docker/login-action@v3.2.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: matrix.registry == 'ghcr.io/home-assistant' if: matrix.registry == 'ghcr.io/home-assistant'
uses: docker/login-action@v3.1.0 uses: docker/login-action@v3.2.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -450,7 +450,7 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
@ -458,7 +458,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations - name: Download translations
uses: actions/download-artifact@v4.1.6 uses: actions/download-artifact@v4.1.7
with: with:
name: translations name: translations

View File

@ -36,7 +36,7 @@ env:
CACHE_VERSION: 8 CACHE_VERSION: 8
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 8 MYPY_CACHE_VERSION: 8
HA_SHORT_VERSION: "2024.5" HA_SHORT_VERSION: "2024.6"
DEFAULT_PYTHON: "3.12" DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12']" ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
@ -89,11 +89,13 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Generate partial Python venv restore key - name: Generate partial Python venv restore key
id: generate_python_cache_key id: generate_python_cache_key
run: >- run: |
echo "key=venv-${{ env.CACHE_VERSION }}-${{ # 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_test.txt', 'requirements_test_pre_commit.txt') }}-${{
hashFiles('requirements.txt') }}-${{ hashFiles('requirements.txt') }}-${{
hashFiles('requirements_all.txt') }}-${{ hashFiles('requirements_all.txt') }}-${{
@ -224,7 +226,7 @@ jobs:
- info - info
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
@ -270,7 +272,7 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
id: python id: python
@ -310,7 +312,7 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
id: python id: python
@ -349,7 +351,7 @@ jobs:
- pre-commit - pre-commit
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
id: python id: python
@ -443,7 +445,7 @@ jobs:
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
@ -520,7 +522,7 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
@ -552,7 +554,7 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
@ -585,7 +587,7 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
@ -609,14 +611,14 @@ jobs:
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
pylint --ignore-missing-annotations=y --ignore-wrong-coordinator-module=y homeassistant pylint --ignore-missing-annotations=y homeassistant
- name: Run pylint (partially) - name: Run pylint (partially)
if: needs.info.outputs.test_full_suite == 'false' if: needs.info.outputs.test_full_suite == 'false'
shell: bash shell: bash
run: | run: |
. venv/bin/activate . venv/bin/activate
python --version python --version
pylint --ignore-missing-annotations=y --ignore-wrong-coordinator-module=y homeassistant/components/${{ needs.info.outputs.integrations_glob }} pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
mypy: mypy:
name: Check mypy name: Check mypy
@ -629,7 +631,7 @@ jobs:
- base - base
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
@ -702,7 +704,7 @@ jobs:
ffmpeg \ ffmpeg \
libgammu-dev libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
@ -763,7 +765,7 @@ jobs:
ffmpeg \ ffmpeg \
libgammu-dev libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
@ -785,7 +787,7 @@ jobs:
run: | run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets - name: Download pytest_buckets
uses: actions/download-artifact@v4.1.6 uses: actions/download-artifact@v4.1.7
with: with:
name: pytest_buckets name: pytest_buckets
- name: Compile English translations - name: Compile English translations
@ -879,7 +881,7 @@ jobs:
ffmpeg \ ffmpeg \
libmariadb-dev-compat libmariadb-dev-compat
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
@ -1002,7 +1004,7 @@ jobs:
ffmpeg \ ffmpeg \
postgresql-server-dev-14 postgresql-server-dev-14
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
@ -1097,14 +1099,14 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.6 uses: actions/download-artifact@v4.1.7
with: with:
pattern: coverage-* pattern: coverage-*
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true' if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v4.3.0 uses: codecov/codecov-action@v4.4.1
with: with:
fail_ci_if_error: true fail_ci_if_error: true
flags: full-suite flags: full-suite
@ -1144,7 +1146,7 @@ jobs:
ffmpeg \ ffmpeg \
libgammu-dev libgammu-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.1.0 uses: actions/setup-python@v5.1.0
@ -1231,14 +1233,14 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.6 uses: actions/download-artifact@v4.1.7
with: with:
pattern: coverage-* pattern: coverage-*
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false' if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v4.3.0 uses: codecov/codecov-action@v4.4.1
with: with:
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

View File

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

View File

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

View File

@ -32,7 +32,7 @@ jobs:
architectures: ${{ steps.info.outputs.architectures }} architectures: ${{ steps.info.outputs.architectures }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
@ -118,15 +118,15 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.6 uses: actions/download-artifact@v4.1.7
with: with:
name: env_file name: env_file
- name: Download requirements_diff - name: Download requirements_diff
uses: actions/download-artifact@v4.1.6 uses: actions/download-artifact@v4.1.7
with: with:
name: requirements_diff name: requirements_diff
@ -156,20 +156,20 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.3 uses: actions/checkout@v4.1.6
- name: Download env_file - name: Download env_file
uses: actions/download-artifact@v4.1.6 uses: actions/download-artifact@v4.1.7
with: with:
name: env_file name: env_file
- name: Download requirements_diff - name: Download requirements_diff
uses: actions/download-artifact@v4.1.6 uses: actions/download-artifact@v4.1.7
with: with:
name: requirements_diff name: requirements_diff
- name: Download requirements_all_wheels - name: Download requirements_all_wheels
uses: actions/download-artifact@v4.1.6 uses: actions/download-artifact@v4.1.7
with: with:
name: requirements_all_wheels name: requirements_all_wheels
@ -211,7 +211,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_old-cython.txt" requirements: "requirements_old-cython.txt"
@ -226,7 +226,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa" requirements: "requirements_all.txtaa"
@ -240,7 +240,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab" requirements: "requirements_all.txtab"
@ -254,7 +254,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm" apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac" requirements: "requirements_all.txtac"

1
.gitignore vendored
View File

@ -34,6 +34,7 @@ Icon
# GITHUB Proposed Python stuff: # GITHUB Proposed Python stuff:
*.py[cod] *.py[cod]
__pycache__
# C extensions # C extensions
*.so *.so

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.1 rev: v0.4.6
hooks: hooks:
- id: ruff - id: ruff
args: args:
@ -8,11 +8,11 @@ repos:
- id: ruff-format - id: ruff-format
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$ files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.2.6 rev: v2.3.0
hooks: hooks:
- id: codespell - id: codespell
args: args:
- --ignore-words-list=additionals,alle,alot,astroid,bund,caf,convencional,currenty,datas,farenheit,falsy,fo,frequence,haa,hass,iif,incomfort,ines,ist,nam,nd,pres,pullrequests,resset,rime,ser,serie,te,technik,ue,unsecure,vor,withing,zar - --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn,pres,ser,ue
- --skip="./.*,*.csv,*.json,*.ambr" - --skip="./.*,*.csv,*.json,*.ambr"
- --quiet-level=2 - --quiet-level=2
exclude_types: [csv, json, html] exclude_types: [csv, json, html]
@ -61,15 +61,15 @@ repos:
name: mypy name: mypy
entry: script/run-in-env.sh mypy entry: script/run-in-env.sh mypy
language: script language: script
types: [python] types_or: [python, pyi]
require_serial: true require_serial: true
files: ^(homeassistant|pylint)/.+\.(py|pyi)$ files: ^(homeassistant|pylint)/.+\.(py|pyi)$
- id: pylint - id: pylint
name: pylint name: pylint
entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y
language: script language: script
types: [python] types_or: [python, pyi]
files: ^homeassistant/.+\.py$ files: ^homeassistant/.+\.(py|pyi)$
- id: gen_requirements_all - id: gen_requirements_all
name: gen_requirements_all name: gen_requirements_all
entry: script/run-in-env.sh python3 -m script.gen_requirements_all entry: script/run-in-env.sh python3 -m script.gen_requirements_all

View File

@ -48,6 +48,7 @@ homeassistant.components.adax.*
homeassistant.components.adguard.* homeassistant.components.adguard.*
homeassistant.components.aftership.* homeassistant.components.aftership.*
homeassistant.components.air_quality.* homeassistant.components.air_quality.*
homeassistant.components.airgradient.*
homeassistant.components.airly.* homeassistant.components.airly.*
homeassistant.components.airnow.* homeassistant.components.airnow.*
homeassistant.components.airq.* homeassistant.components.airq.*
@ -65,7 +66,6 @@ homeassistant.components.alexa.*
homeassistant.components.alpha_vantage.* homeassistant.components.alpha_vantage.*
homeassistant.components.amazon_polly.* homeassistant.components.amazon_polly.*
homeassistant.components.amberelectric.* homeassistant.components.amberelectric.*
homeassistant.components.ambiclimate.*
homeassistant.components.ambient_network.* homeassistant.components.ambient_network.*
homeassistant.components.ambient_station.* homeassistant.components.ambient_station.*
homeassistant.components.amcrest.* homeassistant.components.amcrest.*
@ -84,6 +84,7 @@ homeassistant.components.api.*
homeassistant.components.apple_tv.* homeassistant.components.apple_tv.*
homeassistant.components.apprise.* homeassistant.components.apprise.*
homeassistant.components.aprs.* homeassistant.components.aprs.*
homeassistant.components.apsystems.*
homeassistant.components.aqualogic.* homeassistant.components.aqualogic.*
homeassistant.components.aquostv.* homeassistant.components.aquostv.*
homeassistant.components.aranet.* homeassistant.components.aranet.*
@ -235,6 +236,7 @@ homeassistant.components.homeworks.*
homeassistant.components.http.* homeassistant.components.http.*
homeassistant.components.huawei_lte.* homeassistant.components.huawei_lte.*
homeassistant.components.humidifier.* homeassistant.components.humidifier.*
homeassistant.components.husqvarna_automower.*
homeassistant.components.hydrawise.* homeassistant.components.hydrawise.*
homeassistant.components.hyperion.* homeassistant.components.hyperion.*
homeassistant.components.ibeacon.* homeassistant.components.ibeacon.*
@ -243,6 +245,7 @@ homeassistant.components.image.*
homeassistant.components.image_processing.* homeassistant.components.image_processing.*
homeassistant.components.image_upload.* homeassistant.components.image_upload.*
homeassistant.components.imap.* homeassistant.components.imap.*
homeassistant.components.imgw_pib.*
homeassistant.components.input_button.* homeassistant.components.input_button.*
homeassistant.components.input_select.* homeassistant.components.input_select.*
homeassistant.components.input_text.* homeassistant.components.input_text.*
@ -299,6 +302,7 @@ homeassistant.components.minecraft_server.*
homeassistant.components.mjpeg.* homeassistant.components.mjpeg.*
homeassistant.components.modbus.* homeassistant.components.modbus.*
homeassistant.components.modem_callerid.* homeassistant.components.modem_callerid.*
homeassistant.components.monzo.*
homeassistant.components.moon.* homeassistant.components.moon.*
homeassistant.components.mopeka.* homeassistant.components.mopeka.*
homeassistant.components.motionmount.* homeassistant.components.motionmount.*
@ -337,7 +341,6 @@ homeassistant.components.persistent_notification.*
homeassistant.components.pi_hole.* homeassistant.components.pi_hole.*
homeassistant.components.ping.* homeassistant.components.ping.*
homeassistant.components.plugwise.* homeassistant.components.plugwise.*
homeassistant.components.poolsense.*
homeassistant.components.powerwall.* homeassistant.components.powerwall.*
homeassistant.components.private_ble_device.* homeassistant.components.private_ble_device.*
homeassistant.components.prometheus.* homeassistant.components.prometheus.*
@ -425,6 +428,7 @@ homeassistant.components.tcp.*
homeassistant.components.technove.* homeassistant.components.technove.*
homeassistant.components.tedee.* homeassistant.components.tedee.*
homeassistant.components.text.* homeassistant.components.text.*
homeassistant.components.thethingsnetwork.*
homeassistant.components.threshold.* homeassistant.components.threshold.*
homeassistant.components.tibber.* homeassistant.components.tibber.*
homeassistant.components.tile.* homeassistant.components.tile.*

4
.vscode/tasks.json vendored
View File

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

View File

@ -56,6 +56,8 @@ build.json @home-assistant/supervisor
/tests/components/agent_dvr/ @ispysoftware /tests/components/agent_dvr/ @ispysoftware
/homeassistant/components/air_quality/ @home-assistant/core /homeassistant/components/air_quality/ @home-assistant/core
/tests/components/air_quality/ @home-assistant/core /tests/components/air_quality/ @home-assistant/core
/homeassistant/components/airgradient/ @airgradienthq @joostlek
/tests/components/airgradient/ @airgradienthq @joostlek
/homeassistant/components/airly/ @bieniu /homeassistant/components/airly/ @bieniu
/tests/components/airly/ @bieniu /tests/components/airly/ @bieniu
/homeassistant/components/airnow/ @asymworks /homeassistant/components/airnow/ @asymworks
@ -78,8 +80,8 @@ build.json @home-assistant/supervisor
/tests/components/airzone/ @Noltari /tests/components/airzone/ @Noltari
/homeassistant/components/airzone_cloud/ @Noltari /homeassistant/components/airzone_cloud/ @Noltari
/tests/components/airzone_cloud/ @Noltari /tests/components/airzone_cloud/ @Noltari
/homeassistant/components/aladdin_connect/ @mkmer /homeassistant/components/aladdin_connect/ @swcloudgenie
/tests/components/aladdin_connect/ @mkmer /tests/components/aladdin_connect/ @swcloudgenie
/homeassistant/components/alarm_control_panel/ @home-assistant/core /homeassistant/components/alarm_control_panel/ @home-assistant/core
/tests/components/alarm_control_panel/ @home-assistant/core /tests/components/alarm_control_panel/ @home-assistant/core
/homeassistant/components/alert/ @home-assistant/core @frenck /homeassistant/components/alert/ @home-assistant/core @frenck
@ -88,8 +90,6 @@ build.json @home-assistant/supervisor
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/homeassistant/components/amberelectric/ @madpilot /homeassistant/components/amberelectric/ @madpilot
/tests/components/amberelectric/ @madpilot /tests/components/amberelectric/ @madpilot
/homeassistant/components/ambiclimate/ @danielhiversen
/tests/components/ambiclimate/ @danielhiversen
/homeassistant/components/ambient_network/ @thomaskistler /homeassistant/components/ambient_network/ @thomaskistler
/tests/components/ambient_network/ @thomaskistler /tests/components/ambient_network/ @thomaskistler
/homeassistant/components/ambient_station/ @bachya /homeassistant/components/ambient_station/ @bachya
@ -127,8 +127,10 @@ build.json @home-assistant/supervisor
/tests/components/aprilaire/ @chamberlain2007 /tests/components/aprilaire/ @chamberlain2007
/homeassistant/components/aprs/ @PhilRW /homeassistant/components/aprs/ @PhilRW
/tests/components/aprs/ @PhilRW /tests/components/aprs/ @PhilRW
/homeassistant/components/aranet/ @aschmitz @thecode /homeassistant/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
/tests/components/aranet/ @aschmitz @thecode /tests/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
/homeassistant/components/aranet/ @aschmitz @thecode @anrijs
/tests/components/aranet/ @aschmitz @thecode @anrijs
/homeassistant/components/arcam_fmj/ @elupus /homeassistant/components/arcam_fmj/ @elupus
/tests/components/arcam_fmj/ @elupus /tests/components/arcam_fmj/ @elupus
/homeassistant/components/arris_tg2492lg/ @vanbalken /homeassistant/components/arris_tg2492lg/ @vanbalken
@ -161,6 +163,8 @@ build.json @home-assistant/supervisor
/tests/components/awair/ @ahayworth @danielsjf /tests/components/awair/ @ahayworth @danielsjf
/homeassistant/components/axis/ @Kane610 /homeassistant/components/axis/ @Kane610
/tests/components/axis/ @Kane610 /tests/components/axis/ @Kane610
/homeassistant/components/azure_data_explorer/ @kaareseras
/tests/components/azure_data_explorer/ @kaareseras
/homeassistant/components/azure_devops/ @timmo001 /homeassistant/components/azure_devops/ @timmo001
/tests/components/azure_devops/ @timmo001 /tests/components/azure_devops/ @timmo001
/homeassistant/components/azure_event_hub/ @eavanvalkenburg /homeassistant/components/azure_event_hub/ @eavanvalkenburg
@ -338,8 +342,8 @@ build.json @home-assistant/supervisor
/tests/components/drop_connect/ @ChandlerSystems @pfrazer /tests/components/drop_connect/ @ChandlerSystems @pfrazer
/homeassistant/components/dsmr/ @Robbie1221 @frenck /homeassistant/components/dsmr/ @Robbie1221 @frenck
/tests/components/dsmr/ @Robbie1221 @frenck /tests/components/dsmr/ @Robbie1221 @frenck
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox /homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/tests/components/dsmr_reader/ @sorted-bits @glodenox /tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/homeassistant/components/duotecno/ @cereal2nd /homeassistant/components/duotecno/ @cereal2nd
/tests/components/duotecno/ @cereal2nd /tests/components/duotecno/ @cereal2nd
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
@ -550,14 +554,14 @@ build.json @home-assistant/supervisor
/tests/components/group/ @home-assistant/core /tests/components/group/ @home-assistant/core
/homeassistant/components/guardian/ @bachya /homeassistant/components/guardian/ @bachya
/tests/components/guardian/ @bachya /tests/components/guardian/ @bachya
/homeassistant/components/habitica/ @ASMfreaK @leikoilja /homeassistant/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r
/tests/components/habitica/ @ASMfreaK @leikoilja /tests/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r
/homeassistant/components/hardkernel/ @home-assistant/core /homeassistant/components/hardkernel/ @home-assistant/core
/tests/components/hardkernel/ @home-assistant/core /tests/components/hardkernel/ @home-assistant/core
/homeassistant/components/hardware/ @home-assistant/core /homeassistant/components/hardware/ @home-assistant/core
/tests/components/hardware/ @home-assistant/core /tests/components/hardware/ @home-assistant/core
/homeassistant/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan /homeassistant/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
/tests/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan /tests/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
/homeassistant/components/hassio/ @home-assistant/supervisor /homeassistant/components/hassio/ @home-assistant/supervisor
/tests/components/hassio/ @home-assistant/supervisor /tests/components/hassio/ @home-assistant/supervisor
/homeassistant/components/hdmi_cec/ @inytar /homeassistant/components/hdmi_cec/ @inytar
@ -650,6 +654,8 @@ build.json @home-assistant/supervisor
/tests/components/image_upload/ @home-assistant/core /tests/components/image_upload/ @home-assistant/core
/homeassistant/components/imap/ @jbouwh /homeassistant/components/imap/ @jbouwh
/tests/components/imap/ @jbouwh /tests/components/imap/ @jbouwh
/homeassistant/components/imgw_pib/ @bieniu
/tests/components/imgw_pib/ @bieniu
/homeassistant/components/improv_ble/ @emontnemery /homeassistant/components/improv_ble/ @emontnemery
/tests/components/improv_ble/ @emontnemery /tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @zxdavb /homeassistant/components/incomfort/ @zxdavb
@ -690,6 +696,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/iqvia/ @bachya /homeassistant/components/iqvia/ @bachya
/tests/components/iqvia/ @bachya /tests/components/iqvia/ @bachya
/homeassistant/components/irish_rail_transport/ @ttroy50 /homeassistant/components/irish_rail_transport/ @ttroy50
/homeassistant/components/isal/ @bdraco
/tests/components/isal/ @bdraco
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair /homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair /tests/components/islamic_prayer_times/ @engrbm87 @cpfair
/homeassistant/components/iss/ @DurgNomis-drol /homeassistant/components/iss/ @DurgNomis-drol
@ -865,6 +873,8 @@ build.json @home-assistant/supervisor
/tests/components/moehlenhoff_alpha2/ @j-a-n /tests/components/moehlenhoff_alpha2/ @j-a-n
/homeassistant/components/monoprice/ @etsinko @OnFreund /homeassistant/components/monoprice/ @etsinko @OnFreund
/tests/components/monoprice/ @etsinko @OnFreund /tests/components/monoprice/ @etsinko @OnFreund
/homeassistant/components/monzo/ @jakemartin-icl
/tests/components/monzo/ @jakemartin-icl
/homeassistant/components/moon/ @fabaff @frenck /homeassistant/components/moon/ @fabaff @frenck
/tests/components/moon/ @fabaff @frenck /tests/components/moon/ @fabaff @frenck
/homeassistant/components/mopeka/ @bdraco /homeassistant/components/mopeka/ @bdraco
@ -1271,8 +1281,6 @@ build.json @home-assistant/supervisor
/tests/components/smappee/ @bsmappee /tests/components/smappee/ @bsmappee
/homeassistant/components/smart_meter_texas/ @grahamwetzler /homeassistant/components/smart_meter_texas/ @grahamwetzler
/tests/components/smart_meter_texas/ @grahamwetzler /tests/components/smart_meter_texas/ @grahamwetzler
/homeassistant/components/smartthings/ @andrewsayre
/tests/components/smartthings/ @andrewsayre
/homeassistant/components/smarttub/ @mdz /homeassistant/components/smarttub/ @mdz
/tests/components/smarttub/ @mdz /tests/components/smarttub/ @mdz
/homeassistant/components/smarty/ @z0mbieprocess /homeassistant/components/smarty/ @z0mbieprocess
@ -1359,8 +1367,8 @@ build.json @home-assistant/supervisor
/tests/components/switchbee/ @jafar-atili /tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski /homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski /tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/homeassistant/components/switchbot_cloud/ @SeraphicRav /homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland
/tests/components/switchbot_cloud/ @SeraphicRav /tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland
/homeassistant/components/switcher_kis/ @thecode /homeassistant/components/switcher_kis/ @thecode
/tests/components/switcher_kis/ @thecode /tests/components/switcher_kis/ @thecode
/homeassistant/components/switchmate/ @danielhiversen @qiz-li /homeassistant/components/switchmate/ @danielhiversen @qiz-li
@ -1413,7 +1421,8 @@ build.json @home-assistant/supervisor
/tests/components/thermobeacon/ @bdraco /tests/components/thermobeacon/ @bdraco
/homeassistant/components/thermopro/ @bdraco @h3ss /homeassistant/components/thermopro/ @bdraco @h3ss
/tests/components/thermopro/ @bdraco @h3ss /tests/components/thermopro/ @bdraco @h3ss
/homeassistant/components/thethingsnetwork/ @fabaff /homeassistant/components/thethingsnetwork/ @angelnu
/tests/components/thethingsnetwork/ @angelnu
/homeassistant/components/thread/ @home-assistant/core /homeassistant/components/thread/ @home-assistant/core
/tests/components/thread/ @home-assistant/core /tests/components/thread/ @home-assistant/core
/homeassistant/components/tibber/ @danielhiversen /homeassistant/components/tibber/ @danielhiversen
@ -1477,8 +1486,8 @@ build.json @home-assistant/supervisor
/tests/components/unifi/ @Kane610 /tests/components/unifi/ @Kane610
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL /homeassistant/components/unifi_direct/ @tofuSCHNITZEL
/homeassistant/components/unifiled/ @florisvdk /homeassistant/components/unifiled/ @florisvdk
/homeassistant/components/unifiprotect/ @AngellusMortis @bdraco /homeassistant/components/unifiprotect/ @bdraco
/tests/components/unifiprotect/ @AngellusMortis @bdraco /tests/components/unifiprotect/ @bdraco
/homeassistant/components/upb/ @gwww /homeassistant/components/upb/ @gwww
/tests/components/upb/ @gwww /tests/components/upb/ @gwww
/homeassistant/components/upc_connect/ @pvizeli @fabaff /homeassistant/components/upc_connect/ @pvizeli @fabaff

View File

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

View File

@ -12,7 +12,7 @@ ENV \
ARG QEMU_CPU ARG QEMU_CPU
# Install uv # Install uv
RUN pip3 install uv==0.1.35 RUN pip3 install uv==0.1.43
WORKDIR /usr/src WORKDIR /usr/src

View File

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

View File

@ -7,6 +7,8 @@ Check out `home-assistant.io <https://home-assistant.io>`__ for `a
demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__, demo <https://demo.home-assistant.io>`__, `installation instructions <https://home-assistant.io/getting-started/>`__,
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__. `tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
This is a project of the `Open Home Foundation <https://www.openhomefoundation.org/>`__.
|screenshot-states| |screenshot-states|
Featured integrations Featured integrations

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from contextlib import suppress
import faulthandler import faulthandler
import os import os
import sys import sys
@ -208,8 +209,10 @@ def main() -> int:
exit_code = runner.run(runtime_conf) exit_code = runner.run(runtime_conf)
faulthandler.disable() faulthandler.disable()
if os.path.getsize(fault_file_name) == 0: # It's possible for the fault file to disappear, so suppress obvious errors
os.remove(fault_file_name) with suppress(FileNotFoundError):
if os.path.getsize(fault_file_name) == 0:
os.remove(fault_file_name)
check_threads() check_threads()

View File

@ -28,15 +28,14 @@ from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRA
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
from .models import AuthFlowResult from .models import AuthFlowResult
from .providers import AuthProvider, LoginFlow, auth_provider_from_config from .providers import AuthProvider, LoginFlow, auth_provider_from_config
from .session import SessionManager
EVENT_USER_ADDED = "user_added" EVENT_USER_ADDED = "user_added"
EVENT_USER_UPDATED = "user_updated" EVENT_USER_UPDATED = "user_updated"
EVENT_USER_REMOVED = "user_removed" EVENT_USER_REMOVED = "user_removed"
_MfaModuleDict = dict[str, MultiFactorAuthModule] type _MfaModuleDict = dict[str, MultiFactorAuthModule]
_ProviderKey = tuple[str, str | None] type _ProviderKey = tuple[str, str | None]
_ProviderDict = dict[_ProviderKey, AuthProvider] type _ProviderDict = dict[_ProviderKey, AuthProvider]
class InvalidAuthError(Exception): class InvalidAuthError(Exception):
@ -181,7 +180,6 @@ class AuthManager:
self._remove_expired_job = HassJob( self._remove_expired_job = HassJob(
self._async_remove_expired_refresh_tokens, job_type=HassJobType.Callback self._async_remove_expired_refresh_tokens, job_type=HassJobType.Callback
) )
self.session = SessionManager(hass, self)
async def async_setup(self) -> None: async def async_setup(self) -> None:
"""Set up the auth manager.""" """Set up the auth manager."""
@ -192,7 +190,6 @@ class AuthManager:
) )
) )
self._async_track_next_refresh_token_expiration() self._async_track_next_refresh_token_expiration()
await self.session.async_setup()
@property @property
def auth_providers(self) -> list[AuthProvider]: def auth_providers(self) -> list[AuthProvider]:
@ -519,6 +516,13 @@ class AuthManager:
for revoke_callback in callbacks: for revoke_callback in callbacks:
revoke_callback() revoke_callback()
@callback
def async_set_expiry(
self, refresh_token: models.RefreshToken, *, enable_expiry: bool
) -> None:
"""Enable or disable expiry of a refresh token."""
self._store.async_set_expiry(refresh_token, enable_expiry=enable_expiry)
@callback @callback
def _async_remove_expired_refresh_tokens(self, _: datetime | None = None) -> None: def _async_remove_expired_refresh_tokens(self, _: datetime | None = None) -> None:
"""Remove expired refresh tokens.""" """Remove expired refresh tokens."""

View File

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

View File

@ -16,6 +16,7 @@ from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.importlib import async_import_module from homeassistant.helpers.importlib import async_import_module
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.util.hass_dict import HassKey
MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry() MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry()
@ -29,7 +30,7 @@ MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
DATA_REQS = "mfa_auth_module_reqs_processed" DATA_REQS: HassKey[set[str]] = HassKey("mfa_auth_module_reqs_processed")
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

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

View File

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

View File

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

View File

@ -17,13 +17,14 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.importlib import async_import_module from homeassistant.helpers.importlib import async_import_module
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.util.hass_dict import HassKey
from ..auth_store import AuthStore from ..auth_store import AuthStore
from ..const import MFA_SESSION_EXPIRATION from ..const import MFA_SESSION_EXPIRATION
from ..models import AuthFlowResult, Credentials, RefreshToken, User, UserMeta from ..models import AuthFlowResult, Credentials, RefreshToken, User, UserMeta
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_REQS = "auth_prov_reqs_processed" DATA_REQS: HassKey[set[str]] = HassKey("auth_prov_reqs_processed")
AUTH_PROVIDERS: Registry[str, type[AuthProvider]] = Registry() AUTH_PROVIDERS: Registry[str, type[AuthProvider]] = Registry()

View File

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

View File

@ -1,205 +0,0 @@
"""Session auth module."""
from __future__ import annotations
from datetime import datetime, timedelta
import secrets
from typing import TYPE_CHECKING, Final, TypedDict
from aiohttp.web import Request
from aiohttp_session import Session, get_session, new_session
from cryptography.fernet import Fernet
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.storage import Store
from homeassistant.util import dt as dt_util
from .models import RefreshToken
if TYPE_CHECKING:
from . import AuthManager
TEMP_TIMEOUT = timedelta(minutes=5)
TEMP_TIMEOUT_SECONDS = TEMP_TIMEOUT.total_seconds()
SESSION_ID = "id"
STORAGE_VERSION = 1
STORAGE_KEY = "auth.session"
class StrictConnectionTempSessionData:
"""Data for accessing unauthorized resources for a short period of time."""
__slots__ = ("cancel_remove", "absolute_expiry")
def __init__(self, cancel_remove: CALLBACK_TYPE) -> None:
"""Initialize the temp session data."""
self.cancel_remove: Final[CALLBACK_TYPE] = cancel_remove
self.absolute_expiry: Final[datetime] = dt_util.utcnow() + TEMP_TIMEOUT
class StoreData(TypedDict):
"""Data to store."""
unauthorized_sessions: dict[str, str]
key: str
class SessionManager:
"""Session manager."""
def __init__(self, hass: HomeAssistant, auth: AuthManager) -> None:
"""Initialize the strict connection manager."""
self._auth = auth
self._hass = hass
self._temp_sessions: dict[str, StrictConnectionTempSessionData] = {}
self._strict_connection_sessions: dict[str, str] = {}
self._store = Store[StoreData](
hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
)
self._key: str | None = None
self._refresh_token_revoke_callbacks: dict[str, CALLBACK_TYPE] = {}
@property
def key(self) -> str:
"""Return the encryption key."""
if self._key is None:
self._key = Fernet.generate_key().decode()
self._async_schedule_save()
return self._key
async def async_validate_request_for_strict_connection_session(
self,
request: Request,
) -> bool:
"""Check if a request has a valid strict connection session."""
session = await get_session(request)
if session.new or session.empty:
return False
result = self.async_validate_strict_connection_session(session)
if result is False:
session.invalidate()
return result
@callback
def async_validate_strict_connection_session(
self,
session: Session,
) -> bool:
"""Validate a strict connection session."""
if not (session_id := session.get(SESSION_ID)):
return False
if token_id := self._strict_connection_sessions.get(session_id):
if self._auth.async_get_refresh_token(token_id):
return True
# refresh token is invalid, delete entry
self._strict_connection_sessions.pop(session_id)
self._async_schedule_save()
if data := self._temp_sessions.get(session_id):
if dt_util.utcnow() <= data.absolute_expiry:
return True
# session expired, delete entry
self._temp_sessions.pop(session_id).cancel_remove()
return False
@callback
def _async_register_revoke_token_callback(self, refresh_token_id: str) -> None:
"""Register a callback to revoke all sessions for a refresh token."""
if refresh_token_id in self._refresh_token_revoke_callbacks:
return
@callback
def async_invalidate_auth_sessions() -> None:
"""Invalidate all sessions for a refresh token."""
self._strict_connection_sessions = {
session_id: token_id
for session_id, token_id in self._strict_connection_sessions.items()
if token_id != refresh_token_id
}
self._async_schedule_save()
self._refresh_token_revoke_callbacks[refresh_token_id] = (
self._auth.async_register_revoke_token_callback(
refresh_token_id, async_invalidate_auth_sessions
)
)
async def async_create_session(
self,
request: Request,
refresh_token: RefreshToken,
) -> None:
"""Create new session for given refresh token.
Caller needs to make sure that the refresh token is valid.
By creating a session, we are implicitly revoking all other
sessions for the given refresh token as there is one refresh
token per device/user case.
"""
self._strict_connection_sessions = {
session_id: token_id
for session_id, token_id in self._strict_connection_sessions.items()
if token_id != refresh_token.id
}
self._async_register_revoke_token_callback(refresh_token.id)
session_id = await self._async_create_new_session(request)
self._strict_connection_sessions[session_id] = refresh_token.id
self._async_schedule_save()
async def async_create_temp_unauthorized_session(self, request: Request) -> None:
"""Create a temporary unauthorized session."""
session_id = await self._async_create_new_session(
request, max_age=int(TEMP_TIMEOUT_SECONDS)
)
@callback
def remove(_: datetime) -> None:
self._temp_sessions.pop(session_id, None)
self._temp_sessions[session_id] = StrictConnectionTempSessionData(
async_call_later(self._hass, TEMP_TIMEOUT_SECONDS, remove)
)
async def _async_create_new_session(
self,
request: Request,
*,
max_age: int | None = None,
) -> str:
session_id = secrets.token_hex(64)
session = await new_session(request)
session[SESSION_ID] = session_id
if max_age is not None:
session.max_age = max_age
return session_id
@callback
def _async_schedule_save(self, delay: float = 1) -> None:
"""Save sessions."""
self._store.async_delay_save(self._data_to_save, delay)
@callback
def _data_to_save(self) -> StoreData:
"""Return the data to store."""
return StoreData(
unauthorized_sessions=self._strict_connection_sessions,
key=self.key,
)
async def async_setup(self) -> None:
"""Set up session manager."""
data = await self._store.async_load()
if data is None:
return
self._key = data["key"]
self._strict_connection_sessions = data["unauthorized_sessions"]
for token_id in self._strict_connection_sessions.values():
self._async_register_revoke_token_callback(token_id)

View File

@ -1,9 +1,11 @@
"""Block blocking calls being done in asyncio.""" """Block blocking calls being done in asyncio."""
import builtins
from contextlib import suppress from contextlib import suppress
from http.client import HTTPConnection from http.client import HTTPConnection
import importlib import importlib
import sys import sys
import threading
import time import time
from typing import Any from typing import Any
@ -12,12 +14,21 @@ from .util.loop import protect_loop
_IN_TESTS = "unittest" in sys.modules _IN_TESTS = "unittest" in sys.modules
ALLOWED_FILE_PREFIXES = ("/proc",)
def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool: def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool:
# If the module is already imported, we can ignore it. # If the module is already imported, we can ignore it.
return bool((args := mapped_args.get("args")) and args[0] in sys.modules) 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: def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
# #
# Avoid extracting the stack unless we need to since it # Avoid extracting the stack unless we need to since it
@ -25,7 +36,7 @@ def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
# I/O and we are trying to avoid blocking calls. # I/O and we are trying to avoid blocking calls.
# #
# frame[0] is us # frame[0] is us
# frame[1] is check_loop # frame[1] is raise_for_blocking_call
# frame[2] is protected_loop_func # frame[2] is protected_loop_func
# frame[3] is the offender # frame[3] is the offender
with suppress(ValueError): with suppress(ValueError):
@ -35,21 +46,29 @@ def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
def enable() -> None: def enable() -> None:
"""Enable the detection of blocking calls in the event loop.""" """Enable the detection of blocking calls in the event loop."""
loop_thread_id = threading.get_ident()
# Prevent urllib3 and requests doing I/O in event loop # Prevent urllib3 and requests doing I/O in event loop
HTTPConnection.putrequest = protect_loop( # type: ignore[method-assign] HTTPConnection.putrequest = protect_loop( # type: ignore[method-assign]
HTTPConnection.putrequest HTTPConnection.putrequest, loop_thread_id=loop_thread_id
) )
# Prevent sleeping in event loop. Non-strict since 2022.02 # Prevent sleeping in event loop. Non-strict since 2022.02
time.sleep = protect_loop( time.sleep = protect_loop(
time.sleep, strict=False, check_allowed=_check_sleep_call_allowed time.sleep,
strict=False,
check_allowed=_check_sleep_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)
if not _IN_TESTS: 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 # unittest uses `importlib.import_module` to do mocking
# so we cannot protect it if we are running tests # so we cannot protect it if we are running tests
importlib.import_module = protect_loop( importlib.import_module = protect_loop(
@ -57,4 +76,5 @@ def enable() -> None:
strict_core=False, strict_core=False,
strict=False, strict=False,
check_allowed=_check_import_call_allowed, check_allowed=_check_import_call_allowed,
loop_thread_id=loop_thread_id,
) )

View File

@ -9,6 +9,7 @@ from functools import partial
from itertools import chain from itertools import chain
import logging import logging
import logging.handlers import logging.handlers
import mimetypes
from operator import contains, itemgetter from operator import contains, itemgetter
import os import os
import platform import platform
@ -62,6 +63,7 @@ from .components import (
) )
from .components.sensor import recorder as sensor_recorder # noqa: F401 from .components.sensor import recorder as sensor_recorder # noqa: F401
from .const import ( from .const import (
BASE_PLATFORMS,
FORMAT_DATETIME, FORMAT_DATETIME,
KEY_DATA_LOGGING as DATA_LOGGING, KEY_DATA_LOGGING as DATA_LOGGING,
REQUIRED_NEXT_PYTHON_HA_RELEASE, REQUIRED_NEXT_PYTHON_HA_RELEASE,
@ -84,19 +86,23 @@ from .helpers import (
template, template,
translation, translation,
) )
from .helpers.dispatcher import async_dispatcher_send from .helpers.dispatcher import async_dispatcher_send_internal
from .helpers.storage import get_internal_store_manager from .helpers.storage import get_internal_store_manager
from .helpers.system_info import async_get_system_info from .helpers.system_info import async_get_system_info
from .helpers.typing import ConfigType from .helpers.typing import ConfigType
from .setup import ( from .setup import (
BASE_PLATFORMS, # _setup_started is marked as protected to make it clear
DATA_SETUP_STARTED, # 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, async_get_setup_timings,
async_notify_setup_error, async_notify_setup_error,
async_set_domains_to_be_loaded, async_set_domains_to_be_loaded,
async_setup_component, async_setup_component,
) )
from .util.async_ import create_eager_task from .util.async_ import create_eager_task
from .util.hass_dict import HassKey
from .util.logging import async_activate_log_queue_handler from .util.logging import async_activate_log_queue_handler
from .util.package import async_get_user_site, is_virtual_env from .util.package import async_get_user_site, is_virtual_env
@ -116,7 +122,7 @@ SETUP_ORDER_SORT_KEY = partial(contains, BASE_PLATFORMS)
ERROR_LOG_FILENAME = "home-assistant.log" ERROR_LOG_FILENAME = "home-assistant.log"
# hass.data key for logging information. # hass.data key for logging information.
DATA_REGISTRIES_LOADED = "bootstrap_registries_loaded" DATA_REGISTRIES_LOADED: HassKey[None] = HassKey("bootstrap_registries_loaded")
LOG_SLOW_STARTUP_INTERVAL = 60 LOG_SLOW_STARTUP_INTERVAL = 60
SLOW_STARTUP_CHECK_INTERVAL = 1 SLOW_STARTUP_CHECK_INTERVAL = 1
@ -366,23 +372,24 @@ def open_hass_ui(hass: core.HomeAssistant) -> None:
) )
def _init_blocking_io_modules_in_executor() -> None:
"""Initialize modules that do blocking I/O in executor."""
# Cache the result of platform.uname().processor in the executor.
# Multiple modules call this function at startup which
# executes a blocking subprocess call. This is a problem for the
# asyncio event loop. By priming the cache of uname we can
# avoid the blocking call in the event loop.
_ = platform.uname().processor
# Initialize the mimetypes module to avoid blocking calls
# to the filesystem to load the mime.types file.
mimetypes.init()
async def async_load_base_functionality(hass: core.HomeAssistant) -> None: async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
"""Load the registries and cache the result of platform.uname().processor.""" """Load the registries and modules that will do blocking I/O."""
if DATA_REGISTRIES_LOADED in hass.data: if DATA_REGISTRIES_LOADED in hass.data:
return return
hass.data[DATA_REGISTRIES_LOADED] = None hass.data[DATA_REGISTRIES_LOADED] = None
def _cache_uname_processor() -> None:
"""Cache the result of platform.uname().processor in the executor.
Multiple modules call this function at startup which
executes a blocking subprocess call. This is a problem for the
asyncio event loop. By primeing the cache of uname we can
avoid the blocking call in the event loop.
"""
_ = platform.uname().processor
# Load the registries and cache the result of platform.uname().processor
translation.async_setup(hass) translation.async_setup(hass)
entity.async_setup(hass) entity.async_setup(hass)
template.async_setup(hass) template.async_setup(hass)
@ -395,7 +402,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
create_eager_task(floor_registry.async_load(hass)), create_eager_task(floor_registry.async_load(hass)),
create_eager_task(issue_registry.async_load(hass)), create_eager_task(issue_registry.async_load(hass)),
create_eager_task(label_registry.async_load(hass)), create_eager_task(label_registry.async_load(hass)),
hass.async_add_executor_job(_cache_uname_processor), hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
create_eager_task(template.async_load_custom_templates(hass)), create_eager_task(template.async_load_custom_templates(hass)),
create_eager_task(restore_state.async_load(hass)), create_eager_task(restore_state.async_load(hass)),
create_eager_task(hass.config_entries.async_initialize()), create_eager_task(hass.config_entries.async_initialize()),
@ -425,7 +432,11 @@ async def async_from_config_dict(
if not all( if not all(
await asyncio.gather( await asyncio.gather(
*( *(
create_eager_task(async_setup_component(hass, domain, config)) create_eager_task(
async_setup_component(hass, domain, config),
name=f"bootstrap setup {domain}",
loop=hass.loop,
)
for domain in CORE_INTEGRATIONS for domain in CORE_INTEGRATIONS
) )
) )
@ -679,7 +690,7 @@ class _WatchPendingSetups:
if remaining_with_setup_started: if remaining_with_setup_started:
_LOGGER.debug("Integration remaining: %s", remaining_with_setup_started) _LOGGER.debug("Integration remaining: %s", remaining_with_setup_started)
elif waiting_tasks := self._hass._active_tasks: # pylint: disable=protected-access elif waiting_tasks := self._hass._active_tasks: # noqa: SLF001
_LOGGER.debug("Waiting on tasks: %s", waiting_tasks) _LOGGER.debug("Waiting on tasks: %s", waiting_tasks)
self._async_dispatch(remaining_with_setup_started) self._async_dispatch(remaining_with_setup_started)
if ( if (
@ -699,7 +710,7 @@ class _WatchPendingSetups:
def _async_dispatch(self, remaining_with_setup_started: dict[str, float]) -> None: def _async_dispatch(self, remaining_with_setup_started: dict[str, float]) -> None:
"""Dispatch the signal.""" """Dispatch the signal."""
if remaining_with_setup_started or not self._previous_was_empty: if remaining_with_setup_started or not self._previous_was_empty:
async_dispatcher_send( async_dispatcher_send_internal(
self._hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, remaining_with_setup_started self._hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, remaining_with_setup_started
) )
self._previous_was_empty = not remaining_with_setup_started self._previous_was_empty = not remaining_with_setup_started
@ -916,9 +927,7 @@ async def _async_set_up_integrations(
hass: core.HomeAssistant, config: dict[str, Any] hass: core.HomeAssistant, config: dict[str, Any]
) -> None: ) -> None:
"""Set up all the integrations.""" """Set up all the integrations."""
setup_started: dict[tuple[str, str | None], float] = {} watcher = _WatchPendingSetups(hass, _setup_started(hass))
hass.data[DATA_SETUP_STARTED] = setup_started
watcher = _WatchPendingSetups(hass, setup_started)
watcher.async_start() watcher.async_start()
domains_to_setup, integration_cache = await _async_resolve_domains_to_setup( domains_to_setup, integration_cache = await _async_resolve_domains_to_setup(
@ -985,7 +994,7 @@ async def _async_set_up_integrations(
except TimeoutError: except TimeoutError:
_LOGGER.warning( _LOGGER.warning(
"Setup timed out for stage 1 waiting on %s - moving forward", "Setup timed out for stage 1 waiting on %s - moving forward",
hass._active_tasks, # pylint: disable=protected-access hass._active_tasks, # noqa: SLF001
) )
# Add after dependencies when setting up stage 2 domains # Add after dependencies when setting up stage 2 domains
@ -1001,7 +1010,7 @@ async def _async_set_up_integrations(
except TimeoutError: except TimeoutError:
_LOGGER.warning( _LOGGER.warning(
"Setup timed out for stage 2 waiting on %s - moving forward", "Setup timed out for stage 2 waiting on %s - moving forward",
hass._active_tasks, # pylint: disable=protected-access hass._active_tasks, # noqa: SLF001
) )
# Wrap up startup # Wrap up startup
@ -1012,7 +1021,7 @@ async def _async_set_up_integrations(
except TimeoutError: except TimeoutError:
_LOGGER.warning( _LOGGER.warning(
"Setup timed out for bootstrap waiting on %s - moving forward", "Setup timed out for bootstrap waiting on %s - moving forward",
hass._active_tasks, # pylint: disable=protected-access hass._active_tasks, # noqa: SLF001
) )
watcher.async_stop() watcher.async_stop()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,10 @@ class AccuWeatherData:
coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: type AccuWeatherConfigEntry = ConfigEntry[AccuWeatherData]
async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) -> bool:
"""Set up AccuWeather as config entry.""" """Set up AccuWeather as config entry."""
api_key: str = entry.data[CONF_API_KEY] api_key: str = entry.data[CONF_API_KEY]
name: str = entry.data[CONF_NAME] name: str = entry.data[CONF_NAME]
@ -64,9 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator_observation.async_config_entry_first_refresh() await coordinator_observation.async_config_entry_first_refresh()
await coordinator_daily_forecast.async_config_entry_first_refresh() await coordinator_daily_forecast.async_config_entry_first_refresh()
entry.async_on_unload(entry.add_update_listener(update_listener)) entry.runtime_data = AccuWeatherData(
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AccuWeatherData(
coordinator_observation=coordinator_observation, coordinator_observation=coordinator_observation,
coordinator_daily_forecast=coordinator_daily_forecast, coordinator_daily_forecast=coordinator_daily_forecast,
) )
@ -84,16 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(
hass: HomeAssistant, entry: AccuWeatherConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return 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)

View File

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

View File

@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_PARTS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_CUBIC_METER,
PERCENTAGE, PERCENTAGE,
@ -28,7 +27,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AccuWeatherData from . import AccuWeatherConfigEntry
from .const import ( from .const import (
API_METRIC, API_METRIC,
ATTR_CATEGORY, ATTR_CATEGORY,
@ -38,7 +37,6 @@ from .const import (
ATTR_SPEED, ATTR_SPEED,
ATTR_VALUE, ATTR_VALUE,
ATTRIBUTION, ATTRIBUTION,
DOMAIN,
MAX_FORECAST_DAYS, MAX_FORECAST_DAYS,
) )
from .coordinator import ( from .coordinator import (
@ -458,17 +456,16 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: AccuWeatherConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add AccuWeather entities from a config_entry.""" """Add AccuWeather entities from a config_entry."""
accuweather_data: AccuWeatherData = hass.data[DOMAIN][entry.entry_id]
observation_coordinator: AccuWeatherObservationDataUpdateCoordinator = ( observation_coordinator: AccuWeatherObservationDataUpdateCoordinator = (
accuweather_data.coordinator_observation entry.runtime_data.coordinator_observation
) )
forecast_daily_coordinator: AccuWeatherDailyForecastDataUpdateCoordinator = ( forecast_daily_coordinator: AccuWeatherDailyForecastDataUpdateCoordinator = (
accuweather_data.coordinator_daily_forecast entry.runtime_data.coordinator_daily_forecast
) )
sensors: list[AccuWeatherSensor | AccuWeatherForecastSensor] = [ sensors: list[AccuWeatherSensor | AccuWeatherForecastSensor] = [

View File

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

View File

@ -21,7 +21,6 @@ from homeassistant.components.weather import (
Forecast, Forecast,
WeatherEntityFeature, WeatherEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
UnitOfLength, UnitOfLength,
UnitOfPrecipitationDepth, UnitOfPrecipitationDepth,
@ -31,10 +30,9 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.dt import utc_from_timestamp
from . import AccuWeatherData from . import AccuWeatherConfigEntry, AccuWeatherData
from .const import ( from .const import (
API_METRIC, API_METRIC,
ATTR_DIRECTION, ATTR_DIRECTION,
@ -42,7 +40,6 @@ from .const import (
ATTR_VALUE, ATTR_VALUE,
ATTRIBUTION, ATTRIBUTION,
CONDITION_MAP, CONDITION_MAP,
DOMAIN,
) )
from .coordinator import ( from .coordinator import (
AccuWeatherDailyForecastDataUpdateCoordinator, AccuWeatherDailyForecastDataUpdateCoordinator,
@ -53,20 +50,18 @@ PARALLEL_UPDATES = 1
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: AccuWeatherConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Add a AccuWeather weather entity from a config_entry.""" """Add a AccuWeather weather entity from a config_entry."""
accuweather_data: AccuWeatherData = hass.data[DOMAIN][entry.entry_id] async_add_entities([AccuWeatherEntity(entry.runtime_data)])
async_add_entities([AccuWeatherEntity(accuweather_data)])
class AccuWeatherEntity( class AccuWeatherEntity(
CoordinatorWeatherEntity[ CoordinatorWeatherEntity[
AccuWeatherObservationDataUpdateCoordinator, AccuWeatherObservationDataUpdateCoordinator,
AccuWeatherDailyForecastDataUpdateCoordinator, AccuWeatherDailyForecastDataUpdateCoordinator,
TimestampDataUpdateCoordinator,
TimestampDataUpdateCoordinator,
] ]
): ):
"""Define an AccuWeather entity.""" """Define an AccuWeather entity."""

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ from dataclasses import dataclass
from adguardhome import AdGuardHome, AdGuardHomeConnectionError from adguardhome import AdGuardHome, AdGuardHomeConnectionError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_NAME, CONF_NAME,
@ -43,6 +43,7 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
) )
PLATFORMS = [Platform.SENSOR, Platform.SWITCH] PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
type AdGuardConfigEntry = ConfigEntry[AdGuardData]
@dataclass @dataclass
@ -53,7 +54,7 @@ class AdGuardData:
version: str version: str
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool:
"""Set up AdGuard Home from a config entry.""" """Set up AdGuard Home from a config entry."""
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]) session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
adguard = AdGuardHome( adguard = AdGuardHome(
@ -71,7 +72,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except AdGuardHomeConnectionError as exception: except AdGuardHomeConnectionError as exception:
raise ConfigEntryNotReady from exception raise ConfigEntryNotReady from exception
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AdGuardData(adguard, version) entry.runtime_data = AdGuardData(adguard, version)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -116,17 +117,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: AdGuardConfigEntry) -> bool:
"""Unload AdGuard Home config entry.""" """Unload AdGuard Home config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok: loaded_entries = [
hass.data[DOMAIN].pop(entry.entry_id) entry
if not hass.data[DOMAIN]: 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
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL) hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_REFRESH) hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
del hass.data[DOMAIN]
return unload_ok return unload_ok

View File

@ -4,11 +4,11 @@ from __future__ import annotations
from adguardhome import AdGuardHomeError from adguardhome import AdGuardHomeError
from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry from homeassistant.config_entries import SOURCE_HASSIO
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from . import AdGuardData from . import AdGuardConfigEntry, AdGuardData
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
@ -21,7 +21,7 @@ class AdGuardHomeEntity(Entity):
def __init__( def __init__(
self, self,
data: AdGuardData, data: AdGuardData,
entry: ConfigEntry, entry: AdGuardConfigEntry,
) -> None: ) -> None:
"""Initialize the AdGuard Home entity.""" """Initialize the AdGuard Home entity."""
self._entry = entry self._entry = entry

View File

@ -10,12 +10,11 @@ from typing import Any
from adguardhome import AdGuardHome from adguardhome import AdGuardHome
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfTime from homeassistant.const import PERCENTAGE, UnitOfTime
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdGuardData from . import AdGuardConfigEntry, AdGuardData
from .const import DOMAIN from .const import DOMAIN
from .entity import AdGuardHomeEntity from .entity import AdGuardHomeEntity
@ -85,11 +84,11 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: AdGuardConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdGuard Home sensor based on a config entry.""" """Set up AdGuard Home sensor based on a config entry."""
data: AdGuardData = hass.data[DOMAIN][entry.entry_id] data = entry.runtime_data
async_add_entities( async_add_entities(
[AdGuardHomeSensor(data, entry, description) for description in SENSORS], [AdGuardHomeSensor(data, entry, description) for description in SENSORS],
@ -105,7 +104,7 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity):
def __init__( def __init__(
self, self,
data: AdGuardData, data: AdGuardData,
entry: ConfigEntry, entry: AdGuardConfigEntry,
description: AdGuardHomeEntityDescription, description: AdGuardHomeEntityDescription,
) -> None: ) -> None:
"""Initialize AdGuard Home sensor.""" """Initialize AdGuard Home sensor."""

View File

@ -10,11 +10,10 @@ from typing import Any
from adguardhome import AdGuardHome, AdGuardHomeError from adguardhome import AdGuardHome, AdGuardHomeError
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AdGuardData from . import AdGuardConfigEntry, AdGuardData
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
from .entity import AdGuardHomeEntity from .entity import AdGuardHomeEntity
@ -79,11 +78,11 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: AdGuardConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AdGuard Home switch based on a config entry.""" """Set up AdGuard Home switch based on a config entry."""
data: AdGuardData = hass.data[DOMAIN][entry.entry_id] data = entry.runtime_data
async_add_entities( async_add_entities(
[AdGuardHomeSwitch(data, entry, description) for description in SWITCHES], [AdGuardHomeSwitch(data, entry, description) for description in SWITCHES],
@ -99,7 +98,7 @@ class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity):
def __init__( def __init__(
self, self,
data: AdGuardData, data: AdGuardData,
entry: ConfigEntry, entry: AdGuardConfigEntry,
description: AdGuardHomeSwitchEntityDescription, description: AdGuardHomeSwitchEntityDescription,
) -> None: ) -> None:
"""Initialize AdGuard Home switch.""" """Initialize AdGuard Home switch."""

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/ads", "documentation": "https://www.home-assistant.io/integrations/ads",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyads"], "loggers": ["pyads"],
"requirements": ["pyads==3.2.2"] "requirements": ["pyads==3.4.0"]
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,8 +55,6 @@ CONF_STATION_UPDATES = "station_updates"
PLATFORMS = [Platform.SENSOR, Platform.WEATHER] PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
DEFAULT_NAME = "AEMET" DEFAULT_NAME = "AEMET"
DOMAIN = "aemet" DOMAIN = "aemet"
ENTRY_NAME = "name"
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
ATTR_API_CONDITION = "condition" ATTR_API_CONDITION = "condition"
ATTR_API_FORECAST_CONDITION = "condition" ATTR_API_FORECAST_CONDITION = "condition"

View File

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

View File

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

View File

@ -18,7 +18,6 @@ from homeassistant.components.weather import (
SingleCoordinatorWeatherEntity, SingleCoordinatorWeatherEntity,
WeatherEntityFeature, WeatherEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
UnitOfPrecipitationDepth, UnitOfPrecipitationDepth,
UnitOfPressure, UnitOfPressure,
@ -28,32 +27,24 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from . import AemetConfigEntry
ATTRIBUTION, from .const import ATTRIBUTION, CONDITIONS_MAP
CONDITIONS_MAP,
DOMAIN,
ENTRY_NAME,
ENTRY_WEATHER_COORDINATOR,
)
from .coordinator import WeatherUpdateCoordinator from .coordinator import WeatherUpdateCoordinator
from .entity import AemetEntity from .entity import AemetEntity
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AemetConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AEMET OpenData weather entity based on a config entry.""" """Set up AEMET OpenData weather entity based on a config entry."""
domain_data = hass.data[DOMAIN][config_entry.entry_id] domain_data = config_entry.runtime_data
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] name = domain_data.name
weather_coordinator = domain_data.coordinator
async_add_entities( async_add_entities(
[ [AemetWeather(name, config_entry.unique_id, weather_coordinator)],
AemetWeather(
domain_data[ENTRY_NAME], config_entry.unique_id, weather_coordinator
)
],
False, False,
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType, StateType from homeassistant.helpers.typing import ConfigType, StateType
from . import group as group_pre_import # noqa: F401 from . import group as group_pre_import # noqa: F401
from .const import DOMAIN
_LOGGER: Final = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
@ -33,8 +34,6 @@ ATTR_PM_10: Final = "particulate_matter_10"
ATTR_PM_2_5: Final = "particulate_matter_2_5" ATTR_PM_2_5: Final = "particulate_matter_2_5"
ATTR_SO2: Final = "sulphur_dioxide" ATTR_SO2: Final = "sulphur_dioxide"
DOMAIN: Final = "air_quality"
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
SCAN_INTERVAL: Final = timedelta(seconds=30) SCAN_INTERVAL: Final = timedelta(seconds=30)

View File

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

View File

@ -1,5 +1,7 @@
"""Describe group states.""" """Describe group states."""
from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -7,10 +9,12 @@ from homeassistant.core import HomeAssistant, callback
if TYPE_CHECKING: if TYPE_CHECKING:
from homeassistant.components.group import GroupIntegrationRegistry from homeassistant.components.group import GroupIntegrationRegistry
from .const import DOMAIN
@callback @callback
def async_describe_on_off_states( def async_describe_on_off_states(
hass: HomeAssistant, registry: "GroupIntegrationRegistry" hass: HomeAssistant, registry: GroupIntegrationRegistry
) -> None: ) -> None:
"""Describe group on off states.""" """Describe group on off states."""
registry.exclude_domain() registry.exclude_domain(DOMAIN)

View File

@ -0,0 +1,57 @@
"""The Airgradient integration."""
from __future__ import annotations
from airgradient import AirGradientClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
PLATFORMS: list[Platform] = [Platform.SELECT, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Airgradient from a config entry."""
client = AirGradientClient(
entry.data[CONF_HOST], session=async_get_clientsession(hass)
)
measurement_coordinator = AirGradientMeasurementCoordinator(hass, client)
config_coordinator = AirGradientConfigCoordinator(hass, client)
await measurement_coordinator.async_config_entry_first_refresh()
await config_coordinator.async_config_entry_first_refresh()
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, measurement_coordinator.serial_number)},
manufacturer="AirGradient",
model=measurement_coordinator.data.model,
serial_number=measurement_coordinator.data.serial_number,
sw_version=measurement_coordinator.data.firmware_version,
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
"measurement": measurement_coordinator,
"config": config_coordinator,
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,102 @@
"""Config flow for Airgradient."""
from typing import Any
from airgradient import AirGradientClient, AirGradientError, ConfigurationControl
from awesomeversion import AwesomeVersion
from mashumaro import MissingField
import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_MODEL
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
MIN_VERSION = AwesomeVersion("3.1.1")
class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
"""AirGradient config flow."""
def __init__(self) -> None:
"""Initialize the config flow."""
self.data: dict[str, Any] = {}
self.client: AirGradientClient | None = None
async def set_configuration_source(self) -> None:
"""Set configuration source to local if it hasn't been set yet."""
assert self.client
config = await self.client.get_config()
if config.configuration_control is ConfigurationControl.NOT_INITIALIZED:
await self.client.set_configuration_control(ConfigurationControl.LOCAL)
async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
self.data[CONF_HOST] = host = discovery_info.host
self.data[CONF_MODEL] = discovery_info.properties["model"]
await self.async_set_unique_id(discovery_info.properties["serialno"])
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
if AwesomeVersion(discovery_info.properties["fw_ver"]) < MIN_VERSION:
return self.async_abort(reason="invalid_version")
session = async_get_clientsession(self.hass)
self.client = AirGradientClient(host, session=session)
await self.client.get_current_measures()
self.context["title_placeholders"] = {
"model": self.data[CONF_MODEL],
}
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm discovery."""
if user_input is not None:
await self.set_configuration_source()
return self.async_create_entry(
title=self.data[CONF_MODEL],
data={CONF_HOST: self.data[CONF_HOST]},
)
self._set_confirm_only()
return self.async_show_form(
step_id="discovery_confirm",
description_placeholders={
"model": self.data[CONF_MODEL],
},
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input:
session = async_get_clientsession(self.hass)
self.client = AirGradientClient(user_input[CONF_HOST], session=session)
try:
current_measures = await self.client.get_current_measures()
except AirGradientError:
errors["base"] = "cannot_connect"
except MissingField:
return self.async_abort(reason="invalid_version")
else:
await self.async_set_unique_id(current_measures.serial_number)
self._abort_if_unique_id_configured()
await self.set_configuration_source()
return self.async_create_entry(
title=current_measures.model,
data={CONF_HOST: user_input[CONF_HOST]},
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
errors=errors,
)

View File

@ -0,0 +1,7 @@
"""Constants for the Airgradient integration."""
import logging
DOMAIN = "airgradient"
LOGGER = logging.getLogger(__package__)

View File

@ -0,0 +1,57 @@
"""Define an object to manage fetching AirGradient data."""
from datetime import timedelta
from airgradient import AirGradientClient, AirGradientError, Config, Measures
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER
class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
"""Class to manage fetching AirGradient data."""
_update_interval: timedelta
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, client: AirGradientClient) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
logger=LOGGER,
name=f"AirGradient {client.host}",
update_interval=self._update_interval,
)
self.client = client
assert self.config_entry.unique_id
self.serial_number = self.config_entry.unique_id
async def _async_update_data(self) -> _DataT:
try:
return await self._update_data()
except AirGradientError as error:
raise UpdateFailed(error) from error
async def _update_data(self) -> _DataT:
raise NotImplementedError
class AirGradientMeasurementCoordinator(AirGradientCoordinator[Measures]):
"""Class to manage fetching AirGradient data."""
_update_interval = timedelta(minutes=1)
async def _update_data(self) -> Measures:
return await self.client.get_current_measures()
class AirGradientConfigCoordinator(AirGradientCoordinator[Config]):
"""Class to manage fetching AirGradient data."""
_update_interval = timedelta(minutes=5)
async def _update_data(self) -> Config:
return await self.client.get_config()

View File

@ -0,0 +1,20 @@
"""Base class for AirGradient entities."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AirGradientCoordinator
class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
"""Defines a base AirGradient entity."""
_attr_has_entity_name = True
def __init__(self, coordinator: AirGradientCoordinator) -> None:
"""Initialize airgradient entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.serial_number)},
)

View File

@ -0,0 +1,15 @@
{
"entity": {
"sensor": {
"total_volatile_organic_component_index": {
"default": "mdi:molecule"
},
"nitrogen_index": {
"default": "mdi:molecule"
},
"pm003_count": {
"default": "mdi:blur"
}
}
}
}

View File

@ -0,0 +1,11 @@
{
"domain": "airgradient",
"name": "Airgradient",
"codeowners": ["@airgradienthq", "@joostlek"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airgradient",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["airgradient==0.4.3"],
"zeroconf": ["_airgradient._tcp.local."]
}

View File

@ -0,0 +1,124 @@
"""Support for AirGradient select entities."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from airgradient import AirGradientClient, Config
from airgradient.models import ConfigurationControl, TemperatureUnit
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import AirGradientConfigCoordinator, AirGradientMeasurementCoordinator
from .entity import AirGradientEntity
@dataclass(frozen=True, kw_only=True)
class AirGradientSelectEntityDescription(SelectEntityDescription):
"""Describes AirGradient select entity."""
value_fn: Callable[[Config], str | None]
set_value_fn: Callable[[AirGradientClient, str], Awaitable[None]]
requires_display: bool = False
CONFIG_CONTROL_ENTITY = AirGradientSelectEntityDescription(
key="configuration_control",
translation_key="configuration_control",
options=[ConfigurationControl.CLOUD.value, ConfigurationControl.LOCAL.value],
entity_category=EntityCategory.CONFIG,
value_fn=lambda config: config.configuration_control
if config.configuration_control is not ConfigurationControl.NOT_INITIALIZED
else None,
set_value_fn=lambda client, value: client.set_configuration_control(
ConfigurationControl(value)
),
)
PROTECTED_SELECT_TYPES: tuple[AirGradientSelectEntityDescription, ...] = (
AirGradientSelectEntityDescription(
key="display_temperature_unit",
translation_key="display_temperature_unit",
options=[x.value for x in TemperatureUnit],
entity_category=EntityCategory.CONFIG,
value_fn=lambda config: config.temperature_unit,
set_value_fn=lambda client, value: client.set_temperature_unit(
TemperatureUnit(value)
),
requires_display=True,
),
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up AirGradient select entities based on a config entry."""
config_coordinator: AirGradientConfigCoordinator = hass.data[DOMAIN][
entry.entry_id
]["config"]
measurement_coordinator: AirGradientMeasurementCoordinator = hass.data[DOMAIN][
entry.entry_id
]["measurement"]
entities = [AirGradientSelect(config_coordinator, CONFIG_CONTROL_ENTITY)]
entities.extend(
AirGradientProtectedSelect(config_coordinator, description)
for description in PROTECTED_SELECT_TYPES
if (
description.requires_display
and measurement_coordinator.data.model.startswith("I")
)
)
async_add_entities(entities)
class AirGradientSelect(AirGradientEntity, SelectEntity):
"""Defines an AirGradient select entity."""
entity_description: AirGradientSelectEntityDescription
coordinator: AirGradientConfigCoordinator
def __init__(
self,
coordinator: AirGradientConfigCoordinator,
description: AirGradientSelectEntityDescription,
) -> None:
"""Initialize AirGradient select."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
@property
def current_option(self) -> str | None:
"""Return the state of the select."""
return self.entity_description.value_fn(self.coordinator.data)
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self.entity_description.set_value_fn(self.coordinator.client, option)
await self.coordinator.async_request_refresh()
class AirGradientProtectedSelect(AirGradientSelect):
"""Defines a protected AirGradient select entity."""
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
if (
self.coordinator.data.configuration_control
is not ConfigurationControl.LOCAL
):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_local_configuration",
)
await super().async_select_option(option)

View File

@ -0,0 +1,182 @@
"""Support for AirGradient sensors."""
from collections.abc import Callable
from dataclasses import dataclass
from airgradient.models import Measures
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import DOMAIN
from .coordinator import AirGradientMeasurementCoordinator
from .entity import AirGradientEntity
@dataclass(frozen=True, kw_only=True)
class AirGradientSensorEntityDescription(SensorEntityDescription):
"""Describes AirGradient sensor entity."""
value_fn: Callable[[Measures], StateType]
SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = (
AirGradientSensorEntityDescription(
key="pm01",
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.pm01,
),
AirGradientSensorEntityDescription(
key="pm02",
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.pm02,
),
AirGradientSensorEntityDescription(
key="pm10",
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.pm10,
),
AirGradientSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.ambient_temperature,
),
AirGradientSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.relative_humidity,
),
AirGradientSensorEntityDescription(
key="signal_strength",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
value_fn=lambda status: status.signal_strength,
),
AirGradientSensorEntityDescription(
key="tvoc",
translation_key="total_volatile_organic_component_index",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.total_volatile_organic_component_index,
),
AirGradientSensorEntityDescription(
key="nitrogen_index",
translation_key="nitrogen_index",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.nitrogen_index,
),
AirGradientSensorEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.rco2,
),
AirGradientSensorEntityDescription(
key="pm003",
translation_key="pm003_count",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.pm003_count,
),
AirGradientSensorEntityDescription(
key="nox_raw",
translation_key="raw_nitrogen",
native_unit_of_measurement="ticks",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
value_fn=lambda status: status.raw_nitrogen,
),
AirGradientSensorEntityDescription(
key="tvoc_raw",
translation_key="raw_total_volatile_organic_component",
native_unit_of_measurement="ticks",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
value_fn=lambda status: status.raw_total_volatile_organic_component,
),
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up AirGradient sensor entities based on a config entry."""
coordinator: AirGradientMeasurementCoordinator = hass.data[DOMAIN][entry.entry_id][
"measurement"
]
listener: Callable[[], None] | None = None
not_setup: set[AirGradientSensorEntityDescription] = set(SENSOR_TYPES)
@callback
def add_entities() -> None:
"""Add new entities based on the latest data."""
nonlocal not_setup, listener
sensor_descriptions = not_setup
not_setup = set()
sensors = []
for description in sensor_descriptions:
if description.value_fn(coordinator.data) is None:
not_setup.add(description)
else:
sensors.append(AirGradientSensor(coordinator, description))
if sensors:
async_add_entities(sensors)
if not_setup:
if not listener:
listener = coordinator.async_add_listener(add_entities)
elif listener:
listener()
add_entities()
class AirGradientSensor(AirGradientEntity, SensorEntity):
"""Defines an AirGradient sensor."""
entity_description: AirGradientSensorEntityDescription
coordinator: AirGradientMeasurementCoordinator
def __init__(
self,
coordinator: AirGradientMeasurementCoordinator,
description: AirGradientSensorEntityDescription,
) -> None:
"""Initialize airgradient sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@ -0,0 +1,66 @@
{
"config": {
"flow_title": "{model}",
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of the Airgradient device."
}
},
"discovery_confirm": {
"description": "Do you want to setup {model}?"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"entity": {
"select": {
"configuration_control": {
"name": "Configuration source",
"state": {
"cloud": "Cloud",
"local": "Local"
}
},
"display_temperature_unit": {
"name": "Display temperature unit",
"state": {
"c": "Celsius",
"f": "Fahrenheit"
}
}
},
"sensor": {
"total_volatile_organic_component_index": {
"name": "Total VOC index"
},
"nitrogen_index": {
"name": "Nitrogen index"
},
"pm003_count": {
"name": "PM0.3 count"
},
"raw_total_volatile_organic_component": {
"name": "Raw total VOC"
},
"raw_nitrogen": {
"name": "Raw nitrogen"
}
}
},
"exceptions": {
"no_local_configuration": {
"message": "Device should be configured with local configuration to be able to change settings."
}
}
}

View File

@ -19,8 +19,10 @@ PLATFORMS = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type AirlyConfigEntry = ConfigEntry[AirlyDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: AirlyConfigEntry) -> bool:
"""Set up Airly as config entry.""" """Set up Airly as config entry."""
api_key = entry.data[CONF_API_KEY] api_key = entry.data[CONF_API_KEY]
latitude = entry.data[CONF_LATITUDE] latitude = entry.data[CONF_LATITUDE]
@ -62,8 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) )
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {}) entry.runtime_data = coordinator
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -79,11 +80,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: AirlyConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -55,7 +55,7 @@ def set_update_interval(instances_count: int, requests_remaining: int) -> timede
return interval return interval
class AirlyDataUpdateCoordinator(DataUpdateCoordinator): class AirlyDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]):
"""Define an object to hold Airly data.""" """Define an object to hold Airly data."""
def __init__( def __init__(

View File

@ -5,7 +5,6 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_LATITUDE, CONF_LATITUDE,
@ -14,17 +13,16 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import AirlyDataUpdateCoordinator from . import AirlyConfigEntry
from .const import DOMAIN
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIQUE_ID} TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIQUE_ID}
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: AirlyConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator: AirlyDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = config_entry.runtime_data
return { return {
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),

View File

@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONF_NAME, CONF_NAME,
@ -25,7 +24,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AirlyDataUpdateCoordinator from . import AirlyConfigEntry, AirlyDataUpdateCoordinator
from .const import ( from .const import (
ATTR_ADVICE, ATTR_ADVICE,
ATTR_API_ADVICE, ATTR_API_ADVICE,
@ -174,12 +173,14 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: AirlyConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up Airly sensor entities based on a config entry.""" """Set up Airly sensor entities based on a config entry."""
name = entry.data[CONF_NAME] name = entry.data[CONF_NAME]
coordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
async_add_entities( async_add_entities(
( (

View File

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

View File

@ -15,14 +15,16 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN # noqa: F401
from .coordinator import AirNowDataUpdateCoordinator from .coordinator import AirNowDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR] PLATFORMS = [Platform.SENSOR]
type AirNowConfigEntry = ConfigEntry[AirNowDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bool:
"""Set up AirNow from a config entry.""" """Set up AirNow from a config entry."""
api_key = entry.data[CONF_API_KEY] api_key = entry.data[CONF_API_KEY]
latitude = entry.data[CONF_LATITUDE] latitude = entry.data[CONF_LATITUDE]
@ -44,8 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
# Store Entity and Initialize Platforms # Store Entity and Initialize Platforms
hass.data.setdefault(DOMAIN, {}) entry.runtime_data = coordinator
hass.data[DOMAIN][entry.entry_id] = coordinator
# Listen for option changes # Listen for option changes
entry.async_on_unload(entry.add_update_listener(update_listener)) entry.async_on_unload(entry.add_update_listener(update_listener))
@ -87,14 +88,9 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return 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: async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:

View File

@ -82,7 +82,7 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except InvalidLocation: except InvalidLocation:
errors["base"] = "invalid_location" errors["base"] = "invalid_location"
except Exception: # pylint: disable=broad-except except Exception:
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:

View File

@ -8,6 +8,7 @@ ATTR_API_CATEGORY = "Category"
ATTR_API_CAT_LEVEL = "Number" ATTR_API_CAT_LEVEL = "Number"
ATTR_API_CAT_DESCRIPTION = "Name" ATTR_API_CAT_DESCRIPTION = "Name"
ATTR_API_O3 = "O3" ATTR_API_O3 = "O3"
ATTR_API_PM10 = "PM10"
ATTR_API_PM25 = "PM2.5" ATTR_API_PM25 = "PM2.5"
ATTR_API_POLLUTANT = "Pollutant" ATTR_API_POLLUTANT = "Pollutant"
ATTR_API_REPORT_DATE = "DateObserved" ATTR_API_REPORT_DATE = "DateObserved"

View File

@ -5,7 +5,6 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_LATITUDE, CONF_LATITUDE,
@ -14,8 +13,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import AirNowDataUpdateCoordinator from . import AirNowConfigEntry
from .const import DOMAIN
ATTR_LATITUDE_CAP = "Latitude" ATTR_LATITUDE_CAP = "Latitude"
ATTR_LONGITUDE_CAP = "Longitude" ATTR_LONGITUDE_CAP = "Longitude"
@ -40,10 +38,10 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: AirNowConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator: AirNowDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
return async_redact_data( return async_redact_data(
{ {

View File

@ -4,6 +4,9 @@
"aqi": { "aqi": {
"default": "mdi:blur" "default": "mdi:blur"
}, },
"pm10": {
"default": "mdi:blur"
},
"pm25": { "pm25": {
"default": "mdi:blur" "default": "mdi:blur"
}, },

View File

@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_TIME, ATTR_TIME,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@ -26,12 +25,13 @@ from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import get_time_zone from homeassistant.util.dt import get_time_zone
from . import AirNowDataUpdateCoordinator from . import AirNowConfigEntry, AirNowDataUpdateCoordinator
from .const import ( from .const import (
ATTR_API_AQI, ATTR_API_AQI,
ATTR_API_AQI_DESCRIPTION, ATTR_API_AQI_DESCRIPTION,
ATTR_API_AQI_LEVEL, ATTR_API_AQI_LEVEL,
ATTR_API_O3, ATTR_API_O3,
ATTR_API_PM10,
ATTR_API_PM25, ATTR_API_PM25,
ATTR_API_REPORT_DATE, ATTR_API_REPORT_DATE,
ATTR_API_REPORT_HOUR, ATTR_API_REPORT_HOUR,
@ -88,6 +88,15 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
.isoformat(), .isoformat(),
}, },
), ),
AirNowEntityDescription(
key=ATTR_API_PM10,
translation_key="pm10",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PM10,
value_fn=lambda data: data.get(ATTR_API_PM10),
extra_state_attributes_fn=None,
),
AirNowEntityDescription( AirNowEntityDescription(
key=ATTR_API_PM25, key=ATTR_API_PM25,
translation_key="pm25", translation_key="pm25",
@ -116,11 +125,11 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: AirNowConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up AirNow sensor entities based on a config entry.""" """Set up AirNow sensor entities based on a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = config_entry.runtime_data
entities = [AirNowSensor(coordinator, description) for description in SENSOR_TYPES] entities = [AirNowSensor(coordinator, description) for description in SENSOR_TYPES]

View File

@ -36,7 +36,7 @@
"name": "[%key:component::sensor::entity_component::ozone::name%]" "name": "[%key:component::sensor::entity_component::ozone::name%]"
}, },
"station": { "station": {
"name": "PM2.5 reporting station", "name": "Reporting station",
"state_attributes": { "state_attributes": {
"lat": { "name": "[%key:common::config_flow::data::latitude%]" }, "lat": { "name": "[%key:common::config_flow::data::latitude%]" },
"long": { "name": "[%key:common::config_flow::data::longitude%]" } "long": { "name": "[%key:common::config_flow::data::longitude%]" }

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