mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
2024.6.0 (#118400)
This commit is contained in:
commit
460909a7f6
@ -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
|
||||||
|
89
.coveragerc
89
.coveragerc
@ -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
|
||||||
|
@ -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,
|
||||||
|
26
.github/workflows/builder.yml
vendored
26
.github/workflows/builder.yml
vendored
@ -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
|
||||||
|
|
||||||
|
56
.github/workflows/ci.yaml
vendored
56
.github/workflows/ci.yaml
vendored
@ -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 }}
|
||||||
|
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -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"
|
||||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@ -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
|
||||||
|
24
.github/workflows/wheels.yml
vendored
24
.github/workflows/wheels.yml
vendored
@ -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
1
.gitignore
vendored
@ -34,6 +34,7 @@ Icon
|
|||||||
|
|
||||||
# GITHUB Proposed Python stuff:
|
# GITHUB Proposed Python stuff:
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
__pycache__
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
@ -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
|
||||||
|
@ -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
4
.vscode/tasks.json
vendored
@ -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
|
||||||
|
47
CODEOWNERS
47
CODEOWNERS
@ -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
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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:
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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]
|
||||||
|
@ -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(
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
115
homeassistant/components/abode/entity.py
Normal file
115
homeassistant/components/abode/entity.py
Normal 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()
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
|
@ -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),
|
||||||
|
@ -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] = [
|
||||||
|
@ -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),
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
|
@ -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"):
|
||||||
|
@ -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"):
|
||||||
|
@ -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"):
|
||||||
|
@ -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 {
|
||||||
|
@ -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"):
|
||||||
|
@ -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)
|
||||||
|
@ -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"):
|
||||||
|
@ -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"):
|
||||||
|
@ -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)])
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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"
|
||||||
|
@ -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(),
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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):
|
||||||
|
@ -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)},
|
||||||
|
@ -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"
|
|
||||||
|
@ -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)
|
||||||
|
5
homeassistant/components/air_quality/const.py
Normal file
5
homeassistant/components/air_quality/const.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Constants for the air_quality entity platform."""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
DOMAIN: Final = "air_quality"
|
@ -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)
|
||||||
|
57
homeassistant/components/airgradient/__init__.py
Normal file
57
homeassistant/components/airgradient/__init__.py
Normal 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
|
102
homeassistant/components/airgradient/config_flow.py
Normal file
102
homeassistant/components/airgradient/config_flow.py
Normal 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,
|
||||||
|
)
|
7
homeassistant/components/airgradient/const.py
Normal file
7
homeassistant/components/airgradient/const.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
"""Constants for the Airgradient integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
DOMAIN = "airgradient"
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
57
homeassistant/components/airgradient/coordinator.py
Normal file
57
homeassistant/components/airgradient/coordinator.py
Normal 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()
|
20
homeassistant/components/airgradient/entity.py
Normal file
20
homeassistant/components/airgradient/entity.py
Normal 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)},
|
||||||
|
)
|
15
homeassistant/components/airgradient/icons.json
Normal file
15
homeassistant/components/airgradient/icons.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
homeassistant/components/airgradient/manifest.json
Normal file
11
homeassistant/components/airgradient/manifest.json
Normal 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."]
|
||||||
|
}
|
124
homeassistant/components/airgradient/select.py
Normal file
124
homeassistant/components/airgradient/select.py
Normal 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)
|
182
homeassistant/components/airgradient/sensor.py
Normal file
182
homeassistant/components/airgradient/sensor.py
Normal 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)
|
66
homeassistant/components/airgradient/strings.json
Normal file
66
homeassistant/components/airgradient/strings.json
Normal 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
|
@ -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__(
|
||||||
|
@ -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),
|
||||||
|
@ -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(
|
||||||
(
|
(
|
||||||
|
@ -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(
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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"
|
||||||
|
@ -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(
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,9 @@
|
|||||||
"aqi": {
|
"aqi": {
|
||||||
"default": "mdi:blur"
|
"default": "mdi:blur"
|
||||||
},
|
},
|
||||||
|
"pm10": {
|
||||||
|
"default": "mdi:blur"
|
||||||
|
},
|
||||||
"pm25": {
|
"pm25": {
|
||||||
"default": "mdi:blur"
|
"default": "mdi:blur"
|
||||||
},
|
},
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user