mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 07:07:28 +00:00
2024.6.0 (#118400)
This commit is contained in:
commit
460909a7f6
@ -137,6 +137,7 @@ tests: &tests
|
||||
- tests/syrupy.py
|
||||
- tests/test_util/**
|
||||
- tests/testing_config/**
|
||||
- tests/typing.py
|
||||
- tests/util/**
|
||||
|
||||
other: &other
|
||||
|
89
.coveragerc
89
.coveragerc
@ -58,13 +58,18 @@ omit =
|
||||
homeassistant/components/airvisual/sensor.py
|
||||
homeassistant/components/airvisual_pro/__init__.py
|
||||
homeassistant/components/airvisual_pro/sensor.py
|
||||
homeassistant/components/aladdin_connect/__init__.py
|
||||
homeassistant/components/aladdin_connect/api.py
|
||||
homeassistant/components/aladdin_connect/application_credentials.py
|
||||
homeassistant/components/aladdin_connect/cover.py
|
||||
homeassistant/components/aladdin_connect/sensor.py
|
||||
homeassistant/components/alarmdecoder/__init__.py
|
||||
homeassistant/components/alarmdecoder/alarm_control_panel.py
|
||||
homeassistant/components/alarmdecoder/binary_sensor.py
|
||||
homeassistant/components/alarmdecoder/entity.py
|
||||
homeassistant/components/alarmdecoder/sensor.py
|
||||
homeassistant/components/alpha_vantage/sensor.py
|
||||
homeassistant/components/amazon_polly/*
|
||||
homeassistant/components/ambiclimate/climate.py
|
||||
homeassistant/components/ambient_station/__init__.py
|
||||
homeassistant/components/ambient_station/binary_sensor.py
|
||||
homeassistant/components/ambient_station/entity.py
|
||||
@ -82,6 +87,9 @@ omit =
|
||||
homeassistant/components/aprilaire/climate.py
|
||||
homeassistant/components/aprilaire/coordinator.py
|
||||
homeassistant/components/aprilaire/entity.py
|
||||
homeassistant/components/apsystems/__init__.py
|
||||
homeassistant/components/apsystems/coordinator.py
|
||||
homeassistant/components/apsystems/sensor.py
|
||||
homeassistant/components/aqualogic/*
|
||||
homeassistant/components/aquostv/media_player.py
|
||||
homeassistant/components/arcam_fmj/__init__.py
|
||||
@ -120,7 +128,6 @@ omit =
|
||||
homeassistant/components/baf/switch.py
|
||||
homeassistant/components/baidu/tts.py
|
||||
homeassistant/components/bang_olufsen/__init__.py
|
||||
homeassistant/components/bang_olufsen/const.py
|
||||
homeassistant/components/bang_olufsen/entity.py
|
||||
homeassistant/components/bang_olufsen/media_player.py
|
||||
homeassistant/components/bang_olufsen/util.py
|
||||
@ -192,7 +199,6 @@ omit =
|
||||
homeassistant/components/comelit/__init__.py
|
||||
homeassistant/components/comelit/alarm_control_panel.py
|
||||
homeassistant/components/comelit/climate.py
|
||||
homeassistant/components/comelit/const.py
|
||||
homeassistant/components/comelit/coordinator.py
|
||||
homeassistant/components/comelit/cover.py
|
||||
homeassistant/components/comelit/humidifier.py
|
||||
@ -255,9 +261,6 @@ omit =
|
||||
homeassistant/components/dormakaba_dkey/sensor.py
|
||||
homeassistant/components/dovado/*
|
||||
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/dublin_bus_transport/sensor.py
|
||||
homeassistant/components/dunehd/__init__.py
|
||||
@ -269,7 +272,6 @@ omit =
|
||||
homeassistant/components/duotecno/entity.py
|
||||
homeassistant/components/duotecno/light.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/sensor.py
|
||||
homeassistant/components/dweet/*
|
||||
@ -326,8 +328,7 @@ omit =
|
||||
homeassistant/components/elmax/__init__.py
|
||||
homeassistant/components/elmax/alarm_control_panel.py
|
||||
homeassistant/components/elmax/binary_sensor.py
|
||||
homeassistant/components/elmax/common.py
|
||||
homeassistant/components/elmax/const.py
|
||||
homeassistant/components/elmax/coordinator.py
|
||||
homeassistant/components/elmax/cover.py
|
||||
homeassistant/components/elmax/switch.py
|
||||
homeassistant/components/elv/*
|
||||
@ -370,7 +371,6 @@ omit =
|
||||
homeassistant/components/epson/media_player.py
|
||||
homeassistant/components/eq3btsmart/__init__.py
|
||||
homeassistant/components/eq3btsmart/climate.py
|
||||
homeassistant/components/eq3btsmart/const.py
|
||||
homeassistant/components/eq3btsmart/entity.py
|
||||
homeassistant/components/eq3btsmart/models.py
|
||||
homeassistant/components/escea/__init__.py
|
||||
@ -462,8 +462,8 @@ omit =
|
||||
homeassistant/components/freebox/camera.py
|
||||
homeassistant/components/freebox/home_base.py
|
||||
homeassistant/components/freebox/switch.py
|
||||
homeassistant/components/fritz/common.py
|
||||
homeassistant/components/fritz/device_tracker.py
|
||||
homeassistant/components/fritz/coordinator.py
|
||||
homeassistant/components/fritz/entity.py
|
||||
homeassistant/components/fritz/services.py
|
||||
homeassistant/components/fritz/switch.py
|
||||
homeassistant/components/fritzbox_callmonitor/__init__.py
|
||||
@ -473,10 +473,6 @@ omit =
|
||||
homeassistant/components/frontier_silicon/browse_media.py
|
||||
homeassistant/components/frontier_silicon/media_player.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/garages_amsterdam/__init__.py
|
||||
homeassistant/components/garages_amsterdam/binary_sensor.py
|
||||
@ -505,7 +501,6 @@ omit =
|
||||
homeassistant/components/gpsd/sensor.py
|
||||
homeassistant/components/greenwave/light.py
|
||||
homeassistant/components/growatt_server/__init__.py
|
||||
homeassistant/components/growatt_server/const.py
|
||||
homeassistant/components/growatt_server/sensor.py
|
||||
homeassistant/components/growatt_server/sensor_types/*
|
||||
homeassistant/components/gstreamer/media_player.py
|
||||
@ -519,6 +514,7 @@ omit =
|
||||
homeassistant/components/guardian/util.py
|
||||
homeassistant/components/guardian/valve.py
|
||||
homeassistant/components/habitica/__init__.py
|
||||
homeassistant/components/habitica/coordinator.py
|
||||
homeassistant/components/habitica/sensor.py
|
||||
homeassistant/components/harman_kardon_avr/media_player.py
|
||||
homeassistant/components/harmony/data.py
|
||||
@ -684,6 +680,7 @@ omit =
|
||||
homeassistant/components/konnected/panel.py
|
||||
homeassistant/components/konnected/switch.py
|
||||
homeassistant/components/kostal_plenticore/__init__.py
|
||||
homeassistant/components/kostal_plenticore/coordinator.py
|
||||
homeassistant/components/kostal_plenticore/helper.py
|
||||
homeassistant/components/kostal_plenticore/select.py
|
||||
homeassistant/components/kostal_plenticore/sensor.py
|
||||
@ -731,7 +728,6 @@ omit =
|
||||
homeassistant/components/lookin/sensor.py
|
||||
homeassistant/components/loqed/sensor.py
|
||||
homeassistant/components/luci/device_tracker.py
|
||||
homeassistant/components/luftdaten/sensor.py
|
||||
homeassistant/components/lupusec/__init__.py
|
||||
homeassistant/components/lupusec/alarm_control_panel.py
|
||||
homeassistant/components/lupusec/binary_sensor.py
|
||||
@ -761,6 +757,7 @@ omit =
|
||||
homeassistant/components/matrix/__init__.py
|
||||
homeassistant/components/matrix/notify.py
|
||||
homeassistant/components/matter/__init__.py
|
||||
homeassistant/components/matter/fan.py
|
||||
homeassistant/components/meater/__init__.py
|
||||
homeassistant/components/meater/sensor.py
|
||||
homeassistant/components/medcom_ble/__init__.py
|
||||
@ -787,7 +784,7 @@ omit =
|
||||
homeassistant/components/microbees/application_credentials.py
|
||||
homeassistant/components/microbees/binary_sensor.py
|
||||
homeassistant/components/microbees/button.py
|
||||
homeassistant/components/microbees/const.py
|
||||
homeassistant/components/microbees/climate.py
|
||||
homeassistant/components/microbees/coordinator.py
|
||||
homeassistant/components/microbees/cover.py
|
||||
homeassistant/components/microbees/entity.py
|
||||
@ -795,7 +792,7 @@ omit =
|
||||
homeassistant/components/microbees/sensor.py
|
||||
homeassistant/components/microbees/switch.py
|
||||
homeassistant/components/microsoft/tts.py
|
||||
homeassistant/components/mikrotik/hub.py
|
||||
homeassistant/components/mikrotik/coordinator.py
|
||||
homeassistant/components/mill/climate.py
|
||||
homeassistant/components/mill/sensor.py
|
||||
homeassistant/components/minio/minio_helper.py
|
||||
@ -806,10 +803,10 @@ omit =
|
||||
homeassistant/components/mochad/switch.py
|
||||
homeassistant/components/modem_callerid/button.py
|
||||
homeassistant/components/modem_callerid/sensor.py
|
||||
homeassistant/components/moehlenhoff_alpha2/__init__.py
|
||||
homeassistant/components/moehlenhoff_alpha2/binary_sensor.py
|
||||
homeassistant/components/moehlenhoff_alpha2/climate.py
|
||||
homeassistant/components/moehlenhoff_alpha2/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/coordinator.py
|
||||
homeassistant/components/motion_blinds/cover.py
|
||||
@ -919,9 +916,8 @@ omit =
|
||||
homeassistant/components/notion/util.py
|
||||
homeassistant/components/nsw_fuel_station/sensor.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/sensor.py
|
||||
homeassistant/components/nx584/alarm_control_panel.py
|
||||
homeassistant/components/oasa_telematics/sensor.py
|
||||
homeassistant/components/obihai/__init__.py
|
||||
@ -934,7 +930,7 @@ omit =
|
||||
homeassistant/components/ohmconnect/sensor.py
|
||||
homeassistant/components/ombi/*
|
||||
homeassistant/components/omnilogic/__init__.py
|
||||
homeassistant/components/omnilogic/common.py
|
||||
homeassistant/components/omnilogic/coordinator.py
|
||||
homeassistant/components/omnilogic/sensor.py
|
||||
homeassistant/components/omnilogic/switch.py
|
||||
homeassistant/components/ondilo_ico/__init__.py
|
||||
@ -962,7 +958,6 @@ omit =
|
||||
homeassistant/components/opengarage/sensor.py
|
||||
homeassistant/components/openhardwaremonitor/sensor.py
|
||||
homeassistant/components/openhome/__init__.py
|
||||
homeassistant/components/openhome/const.py
|
||||
homeassistant/components/openhome/media_player.py
|
||||
homeassistant/components/opensensemap/air_quality.py
|
||||
homeassistant/components/opentherm_gw/__init__.py
|
||||
@ -974,9 +969,10 @@ omit =
|
||||
homeassistant/components/openuv/coordinator.py
|
||||
homeassistant/components/openuv/sensor.py
|
||||
homeassistant/components/openweathermap/__init__.py
|
||||
homeassistant/components/openweathermap/coordinator.py
|
||||
homeassistant/components/openweathermap/repairs.py
|
||||
homeassistant/components/openweathermap/sensor.py
|
||||
homeassistant/components/openweathermap/weather.py
|
||||
homeassistant/components/openweathermap/weather_update_coordinator.py
|
||||
homeassistant/components/opnsense/__init__.py
|
||||
homeassistant/components/opnsense/device_tracker.py
|
||||
homeassistant/components/opower/__init__.py
|
||||
@ -986,7 +982,7 @@ omit =
|
||||
homeassistant/components/oru/*
|
||||
homeassistant/components/orvibo/switch.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/water_heater.py
|
||||
homeassistant/components/osramlightify/light.py
|
||||
@ -1023,6 +1019,7 @@ omit =
|
||||
homeassistant/components/permobil/entity.py
|
||||
homeassistant/components/permobil/sensor.py
|
||||
homeassistant/components/philips_js/__init__.py
|
||||
homeassistant/components/philips_js/coordinator.py
|
||||
homeassistant/components/philips_js/light.py
|
||||
homeassistant/components/philips_js/media_player.py
|
||||
homeassistant/components/philips_js/remote.py
|
||||
@ -1031,7 +1028,6 @@ omit =
|
||||
homeassistant/components/picotts/tts.py
|
||||
homeassistant/components/pilight/base_class.py
|
||||
homeassistant/components/pilight/binary_sensor.py
|
||||
homeassistant/components/pilight/const.py
|
||||
homeassistant/components/pilight/light.py
|
||||
homeassistant/components/pilight/switch.py
|
||||
homeassistant/components/ping/__init__.py
|
||||
@ -1050,11 +1046,6 @@ omit =
|
||||
homeassistant/components/point/alarm_control_panel.py
|
||||
homeassistant/components/point/binary_sensor.py
|
||||
homeassistant/components/point/sensor.py
|
||||
homeassistant/components/poolsense/__init__.py
|
||||
homeassistant/components/poolsense/binary_sensor.py
|
||||
homeassistant/components/poolsense/coordinator.py
|
||||
homeassistant/components/poolsense/entity.py
|
||||
homeassistant/components/poolsense/sensor.py
|
||||
homeassistant/components/powerwall/__init__.py
|
||||
homeassistant/components/progettihwsw/__init__.py
|
||||
homeassistant/components/progettihwsw/binary_sensor.py
|
||||
@ -1081,7 +1072,6 @@ omit =
|
||||
homeassistant/components/quantum_gateway/device_tracker.py
|
||||
homeassistant/components/qvr_pro/*
|
||||
homeassistant/components/rabbitair/__init__.py
|
||||
homeassistant/components/rabbitair/const.py
|
||||
homeassistant/components/rabbitair/coordinator.py
|
||||
homeassistant/components/rabbitair/entity.py
|
||||
homeassistant/components/rabbitair/fan.py
|
||||
@ -1104,6 +1094,7 @@ omit =
|
||||
homeassistant/components/rainmachine/__init__.py
|
||||
homeassistant/components/rainmachine/binary_sensor.py
|
||||
homeassistant/components/rainmachine/button.py
|
||||
homeassistant/components/rainmachine/coordinator.py
|
||||
homeassistant/components/rainmachine/select.py
|
||||
homeassistant/components/rainmachine/sensor.py
|
||||
homeassistant/components/rainmachine/switch.py
|
||||
@ -1126,7 +1117,6 @@ omit =
|
||||
homeassistant/components/renson/__init__.py
|
||||
homeassistant/components/renson/binary_sensor.py
|
||||
homeassistant/components/renson/button.py
|
||||
homeassistant/components/renson/const.py
|
||||
homeassistant/components/renson/coordinator.py
|
||||
homeassistant/components/renson/entity.py
|
||||
homeassistant/components/renson/fan.py
|
||||
@ -1195,13 +1185,11 @@ omit =
|
||||
homeassistant/components/schluter/*
|
||||
homeassistant/components/screenlogic/binary_sensor.py
|
||||
homeassistant/components/screenlogic/climate.py
|
||||
homeassistant/components/screenlogic/const.py
|
||||
homeassistant/components/screenlogic/coordinator.py
|
||||
homeassistant/components/screenlogic/entity.py
|
||||
homeassistant/components/screenlogic/light.py
|
||||
homeassistant/components/screenlogic/number.py
|
||||
homeassistant/components/screenlogic/sensor.py
|
||||
homeassistant/components/screenlogic/services.py
|
||||
homeassistant/components/screenlogic/switch.py
|
||||
homeassistant/components/scsgate/*
|
||||
homeassistant/components/sendgrid/notify.py
|
||||
@ -1254,7 +1242,6 @@ omit =
|
||||
homeassistant/components/smappee/switch.py
|
||||
homeassistant/components/smarty/*
|
||||
homeassistant/components/sms/__init__.py
|
||||
homeassistant/components/sms/const.py
|
||||
homeassistant/components/sms/coordinator.py
|
||||
homeassistant/components/sms/gateway.py
|
||||
homeassistant/components/sms/notify.py
|
||||
@ -1348,6 +1335,7 @@ omit =
|
||||
homeassistant/components/supla/*
|
||||
homeassistant/components/surepetcare/__init__.py
|
||||
homeassistant/components/surepetcare/binary_sensor.py
|
||||
homeassistant/components/surepetcare/coordinator.py
|
||||
homeassistant/components/surepetcare/entity.py
|
||||
homeassistant/components/surepetcare/sensor.py
|
||||
homeassistant/components/swiss_hydrological_data/sensor.py
|
||||
@ -1376,6 +1364,7 @@ omit =
|
||||
homeassistant/components/switchbot_cloud/climate.py
|
||||
homeassistant/components/switchbot_cloud/coordinator.py
|
||||
homeassistant/components/switchbot_cloud/entity.py
|
||||
homeassistant/components/switchbot_cloud/sensor.py
|
||||
homeassistant/components/switchbot_cloud/switch.py
|
||||
homeassistant/components/switchmate/switch.py
|
||||
homeassistant/components/syncthing/__init__.py
|
||||
@ -1434,12 +1423,11 @@ omit =
|
||||
homeassistant/components/tensorflow/image_processing.py
|
||||
homeassistant/components/tfiac/climate.py
|
||||
homeassistant/components/thermoworks_smoke/sensor.py
|
||||
homeassistant/components/thethingsnetwork/*
|
||||
homeassistant/components/thingspeak/*
|
||||
homeassistant/components/thinkingcleaner/*
|
||||
homeassistant/components/thomson/device_tracker.py
|
||||
homeassistant/components/tibber/__init__.py
|
||||
homeassistant/components/tibber/notify.py
|
||||
homeassistant/components/tibber/coordinator.py
|
||||
homeassistant/components/tibber/sensor.py
|
||||
homeassistant/components/tikteck/light.py
|
||||
homeassistant/components/tile/__init__.py
|
||||
@ -1545,8 +1533,9 @@ omit =
|
||||
homeassistant/components/v2c/coordinator.py
|
||||
homeassistant/components/v2c/entity.py
|
||||
homeassistant/components/v2c/number.py
|
||||
homeassistant/components/v2c/sensor.py
|
||||
homeassistant/components/v2c/switch.py
|
||||
homeassistant/components/vallox/__init__.py
|
||||
homeassistant/components/vallox/coordinator.py
|
||||
homeassistant/components/vasttrafik/sensor.py
|
||||
homeassistant/components/velbus/__init__.py
|
||||
homeassistant/components/velbus/binary_sensor.py
|
||||
@ -1561,9 +1550,8 @@ omit =
|
||||
homeassistant/components/velux/__init__.py
|
||||
homeassistant/components/velux/cover.py
|
||||
homeassistant/components/velux/light.py
|
||||
homeassistant/components/venstar/__init__.py
|
||||
homeassistant/components/venstar/binary_sensor.py
|
||||
homeassistant/components/venstar/climate.py
|
||||
homeassistant/components/venstar/coordinator.py
|
||||
homeassistant/components/venstar/sensor.py
|
||||
homeassistant/components/verisure/__init__.py
|
||||
homeassistant/components/verisure/alarm_control_panel.py
|
||||
@ -1596,7 +1584,6 @@ omit =
|
||||
homeassistant/components/vlc_telnet/media_player.py
|
||||
homeassistant/components/vodafone_station/__init__.py
|
||||
homeassistant/components/vodafone_station/button.py
|
||||
homeassistant/components/vodafone_station/const.py
|
||||
homeassistant/components/vodafone_station/coordinator.py
|
||||
homeassistant/components/vodafone_station/device_tracker.py
|
||||
homeassistant/components/vodafone_station/sensor.py
|
||||
@ -1621,10 +1608,8 @@ omit =
|
||||
homeassistant/components/watttime/__init__.py
|
||||
homeassistant/components/watttime/sensor.py
|
||||
homeassistant/components/weatherflow/__init__.py
|
||||
homeassistant/components/weatherflow/const.py
|
||||
homeassistant/components/weatherflow/sensor.py
|
||||
homeassistant/components/weatherflow_cloud/__init__.py
|
||||
homeassistant/components/weatherflow_cloud/const.py
|
||||
homeassistant/components/weatherflow_cloud/coordinator.py
|
||||
homeassistant/components/weatherflow_cloud/weather.py
|
||||
homeassistant/components/wiffi/__init__.py
|
||||
@ -1642,6 +1627,7 @@ omit =
|
||||
homeassistant/components/xbox/base_sensor.py
|
||||
homeassistant/components/xbox/binary_sensor.py
|
||||
homeassistant/components/xbox/browse_media.py
|
||||
homeassistant/components/xbox/coordinator.py
|
||||
homeassistant/components/xbox/media_player.py
|
||||
homeassistant/components/xbox/remote.py
|
||||
homeassistant/components/xbox/sensor.py
|
||||
@ -1674,10 +1660,7 @@ omit =
|
||||
homeassistant/components/xs1/*
|
||||
homeassistant/components/yale_smart_alarm/__init__.py
|
||||
homeassistant/components/yale_smart_alarm/alarm_control_panel.py
|
||||
homeassistant/components/yale_smart_alarm/binary_sensor.py
|
||||
homeassistant/components/yale_smart_alarm/button.py
|
||||
homeassistant/components/yale_smart_alarm/entity.py
|
||||
homeassistant/components/yale_smart_alarm/lock.py
|
||||
homeassistant/components/yalexs_ble/__init__.py
|
||||
homeassistant/components/yalexs_ble/binary_sensor.py
|
||||
homeassistant/components/yalexs_ble/entity.py
|
||||
@ -1718,10 +1701,6 @@ omit =
|
||||
homeassistant/components/zeroconf/models.py
|
||||
homeassistant/components/zeroconf/usage.py
|
||||
homeassistant/components/zestimate/sensor.py
|
||||
homeassistant/components/zeversolar/__init__.py
|
||||
homeassistant/components/zeversolar/coordinator.py
|
||||
homeassistant/components/zeversolar/entity.py
|
||||
homeassistant/components/zeversolar/sensor.py
|
||||
homeassistant/components/zha/core/cluster_handlers/*
|
||||
homeassistant/components/zha/core/device.py
|
||||
homeassistant/components/zha/core/gateway.py
|
||||
|
@ -5,9 +5,11 @@
|
||||
"postCreateCommand": "script/setup",
|
||||
"postStartCommand": "script/bootstrap",
|
||||
"containerEnv": {
|
||||
"DEVCONTAINER": "1",
|
||||
"PYTHONASYNCIODEBUG": "1"
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
// Port 5683 udp is used by Shelly integration
|
||||
"appPort": ["8123:8123", "5683:5683/udp"],
|
||||
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
|
||||
@ -20,12 +22,15 @@
|
||||
"visualstudioexptteam.vscodeintellicode",
|
||||
"redhat.vscode-yaml",
|
||||
"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
|
||||
"settings": {
|
||||
"python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||
"python.pythonPath": "/usr/local/bin/python",
|
||||
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
|
||||
"python.pythonPath": "/home/vscode/.local/ha-venv/bin/python",
|
||||
"python.terminal.activateEnvInCurrentTerminal": true,
|
||||
"python.testing.pytestArgs": ["--no-cov"],
|
||||
"editor.formatOnPaste": false,
|
||||
"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 }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -90,7 +90,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
@ -175,7 +175,7 @@ jobs:
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.1.6
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@ -190,7 +190,7 @@ jobs:
|
||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.1.0
|
||||
uses: docker/login-action@v3.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@ -242,7 +242,7 @@ jobs:
|
||||
- green
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
- name: Set build additional args
|
||||
run: |
|
||||
@ -256,7 +256,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3.1.0
|
||||
uses: docker/login-action@v3.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@ -279,7 +279,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master
|
||||
@ -320,23 +320,23 @@ jobs:
|
||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.4.0
|
||||
uses: sigstore/cosign-installer@v3.5.0
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@v3.1.0
|
||||
uses: docker/login-action@v3.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||
uses: docker/login-action@v3.1.0
|
||||
uses: docker/login-action@v3.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@ -450,7 +450,7 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
@ -458,7 +458,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.1.6
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
name: translations
|
||||
|
||||
|
56
.github/workflows/ci.yaml
vendored
56
.github/workflows/ci.yaml
vendored
@ -36,7 +36,7 @@ env:
|
||||
CACHE_VERSION: 8
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 8
|
||||
HA_SHORT_VERSION: "2024.5"
|
||||
HA_SHORT_VERSION: "2024.6"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||
# 10.3 is the oldest supported version
|
||||
@ -89,11 +89,13 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- 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
|
||||
id: generate_python_cache_key
|
||||
run: >-
|
||||
echo "key=venv-${{ env.CACHE_VERSION }}-${{
|
||||
run: |
|
||||
# Include HA_SHORT_VERSION to force the immediate creation
|
||||
# of a new uv cache entry after a version bump.
|
||||
echo "key=venv-${{ env.CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}-${{
|
||||
hashFiles('requirements_test.txt', 'requirements_test_pre_commit.txt') }}-${{
|
||||
hashFiles('requirements.txt') }}-${{
|
||||
hashFiles('requirements_all.txt') }}-${{
|
||||
@ -224,7 +226,7 @@ jobs:
|
||||
- info
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
@ -270,7 +272,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- 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 }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
id: python
|
||||
@ -310,7 +312,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- 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 }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
id: python
|
||||
@ -349,7 +351,7 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- 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 }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
id: python
|
||||
@ -443,7 +445,7 @@ jobs:
|
||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
@ -520,7 +522,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
@ -552,7 +554,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
@ -585,7 +587,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
@ -609,14 +611,14 @@ jobs:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pylint --ignore-missing-annotations=y --ignore-wrong-coordinator-module=y homeassistant
|
||||
pylint --ignore-missing-annotations=y homeassistant
|
||||
- name: Run pylint (partially)
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pylint --ignore-missing-annotations=y --ignore-wrong-coordinator-module=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
||||
pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
||||
|
||||
mypy:
|
||||
name: Check mypy
|
||||
@ -629,7 +631,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
@ -702,7 +704,7 @@ jobs:
|
||||
ffmpeg \
|
||||
libgammu-dev
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
@ -763,7 +765,7 @@ jobs:
|
||||
ffmpeg \
|
||||
libgammu-dev
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
@ -785,7 +787,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: actions/download-artifact@v4.1.6
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
name: pytest_buckets
|
||||
- name: Compile English translations
|
||||
@ -879,7 +881,7 @@ jobs:
|
||||
ffmpeg \
|
||||
libmariadb-dev-compat
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
@ -1002,7 +1004,7 @@ jobs:
|
||||
ffmpeg \
|
||||
postgresql-server-dev-14
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
@ -1097,14 +1099,14 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.6
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
uses: codecov/codecov-action@v4.3.0
|
||||
uses: codecov/codecov-action@v4.4.1
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
@ -1144,7 +1146,7 @@ jobs:
|
||||
ffmpeg \
|
||||
libgammu-dev
|
||||
- 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 }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
@ -1231,14 +1233,14 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.1.6
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
uses: codecov/codecov-action@v4.3.0
|
||||
uses: codecov/codecov-action@v4.4.1
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -21,14 +21,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.25.2
|
||||
uses: github/codeql-action/init@v3.25.6
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.25.2
|
||||
uses: github/codeql-action/analyze@v3.25.6
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
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 }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
@ -118,15 +118,15 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.6
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.1.6
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@ -156,20 +156,20 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.3
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.1.6
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.1.6
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@v4.1.6
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
@ -211,7 +211,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_old-cython.txt"
|
||||
@ -226,7 +226,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtaa"
|
||||
@ -240,7 +240,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtab"
|
||||
@ -254,7 +254,7 @@ jobs:
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtac"
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -34,6 +34,7 @@ Icon
|
||||
|
||||
# GITHUB Proposed Python stuff:
|
||||
*.py[cod]
|
||||
__pycache__
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.4.1
|
||||
rev: v0.4.6
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
@ -8,11 +8,11 @@ repos:
|
||||
- id: ruff-format
|
||||
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.6
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
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"
|
||||
- --quiet-level=2
|
||||
exclude_types: [csv, json, html]
|
||||
@ -61,15 +61,15 @@ repos:
|
||||
name: mypy
|
||||
entry: script/run-in-env.sh mypy
|
||||
language: script
|
||||
types: [python]
|
||||
types_or: [python, pyi]
|
||||
require_serial: true
|
||||
files: ^(homeassistant|pylint)/.+\.(py|pyi)$
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y
|
||||
language: script
|
||||
types: [python]
|
||||
files: ^homeassistant/.+\.py$
|
||||
types_or: [python, pyi]
|
||||
files: ^homeassistant/.+\.(py|pyi)$
|
||||
- id: gen_requirements_all
|
||||
name: 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.aftership.*
|
||||
homeassistant.components.air_quality.*
|
||||
homeassistant.components.airgradient.*
|
||||
homeassistant.components.airly.*
|
||||
homeassistant.components.airnow.*
|
||||
homeassistant.components.airq.*
|
||||
@ -65,7 +66,6 @@ homeassistant.components.alexa.*
|
||||
homeassistant.components.alpha_vantage.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.amberelectric.*
|
||||
homeassistant.components.ambiclimate.*
|
||||
homeassistant.components.ambient_network.*
|
||||
homeassistant.components.ambient_station.*
|
||||
homeassistant.components.amcrest.*
|
||||
@ -84,6 +84,7 @@ homeassistant.components.api.*
|
||||
homeassistant.components.apple_tv.*
|
||||
homeassistant.components.apprise.*
|
||||
homeassistant.components.aprs.*
|
||||
homeassistant.components.apsystems.*
|
||||
homeassistant.components.aqualogic.*
|
||||
homeassistant.components.aquostv.*
|
||||
homeassistant.components.aranet.*
|
||||
@ -235,6 +236,7 @@ homeassistant.components.homeworks.*
|
||||
homeassistant.components.http.*
|
||||
homeassistant.components.huawei_lte.*
|
||||
homeassistant.components.humidifier.*
|
||||
homeassistant.components.husqvarna_automower.*
|
||||
homeassistant.components.hydrawise.*
|
||||
homeassistant.components.hyperion.*
|
||||
homeassistant.components.ibeacon.*
|
||||
@ -243,6 +245,7 @@ homeassistant.components.image.*
|
||||
homeassistant.components.image_processing.*
|
||||
homeassistant.components.image_upload.*
|
||||
homeassistant.components.imap.*
|
||||
homeassistant.components.imgw_pib.*
|
||||
homeassistant.components.input_button.*
|
||||
homeassistant.components.input_select.*
|
||||
homeassistant.components.input_text.*
|
||||
@ -299,6 +302,7 @@ homeassistant.components.minecraft_server.*
|
||||
homeassistant.components.mjpeg.*
|
||||
homeassistant.components.modbus.*
|
||||
homeassistant.components.modem_callerid.*
|
||||
homeassistant.components.monzo.*
|
||||
homeassistant.components.moon.*
|
||||
homeassistant.components.mopeka.*
|
||||
homeassistant.components.motionmount.*
|
||||
@ -337,7 +341,6 @@ homeassistant.components.persistent_notification.*
|
||||
homeassistant.components.pi_hole.*
|
||||
homeassistant.components.ping.*
|
||||
homeassistant.components.plugwise.*
|
||||
homeassistant.components.poolsense.*
|
||||
homeassistant.components.powerwall.*
|
||||
homeassistant.components.private_ble_device.*
|
||||
homeassistant.components.prometheus.*
|
||||
@ -425,6 +428,7 @@ homeassistant.components.tcp.*
|
||||
homeassistant.components.technove.*
|
||||
homeassistant.components.tedee.*
|
||||
homeassistant.components.text.*
|
||||
homeassistant.components.thethingsnetwork.*
|
||||
homeassistant.components.threshold.*
|
||||
homeassistant.components.tibber.*
|
||||
homeassistant.components.tile.*
|
||||
|
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
@ -103,7 +103,7 @@
|
||||
{
|
||||
"label": "Install all Requirements",
|
||||
"type": "shell",
|
||||
"command": "pip3 install -r requirements_all.txt",
|
||||
"command": "uv pip install -r requirements_all.txt",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
@ -117,7 +117,7 @@
|
||||
{
|
||||
"label": "Install all Test Requirements",
|
||||
"type": "shell",
|
||||
"command": "pip3 install -r requirements_test_all.txt",
|
||||
"command": "uv pip install -r requirements_test_all.txt",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
47
CODEOWNERS
47
CODEOWNERS
@ -56,6 +56,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/agent_dvr/ @ispysoftware
|
||||
/homeassistant/components/air_quality/ @home-assistant/core
|
||||
/tests/components/air_quality/ @home-assistant/core
|
||||
/homeassistant/components/airgradient/ @airgradienthq @joostlek
|
||||
/tests/components/airgradient/ @airgradienthq @joostlek
|
||||
/homeassistant/components/airly/ @bieniu
|
||||
/tests/components/airly/ @bieniu
|
||||
/homeassistant/components/airnow/ @asymworks
|
||||
@ -78,8 +80,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/airzone/ @Noltari
|
||||
/homeassistant/components/airzone_cloud/ @Noltari
|
||||
/tests/components/airzone_cloud/ @Noltari
|
||||
/homeassistant/components/aladdin_connect/ @mkmer
|
||||
/tests/components/aladdin_connect/ @mkmer
|
||||
/homeassistant/components/aladdin_connect/ @swcloudgenie
|
||||
/tests/components/aladdin_connect/ @swcloudgenie
|
||||
/homeassistant/components/alarm_control_panel/ @home-assistant/core
|
||||
/tests/components/alarm_control_panel/ @home-assistant/core
|
||||
/homeassistant/components/alert/ @home-assistant/core @frenck
|
||||
@ -88,8 +90,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/homeassistant/components/amberelectric/ @madpilot
|
||||
/tests/components/amberelectric/ @madpilot
|
||||
/homeassistant/components/ambiclimate/ @danielhiversen
|
||||
/tests/components/ambiclimate/ @danielhiversen
|
||||
/homeassistant/components/ambient_network/ @thomaskistler
|
||||
/tests/components/ambient_network/ @thomaskistler
|
||||
/homeassistant/components/ambient_station/ @bachya
|
||||
@ -127,8 +127,10 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/aprilaire/ @chamberlain2007
|
||||
/homeassistant/components/aprs/ @PhilRW
|
||||
/tests/components/aprs/ @PhilRW
|
||||
/homeassistant/components/aranet/ @aschmitz @thecode
|
||||
/tests/components/aranet/ @aschmitz @thecode
|
||||
/homeassistant/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
|
||||
/tests/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
|
||||
/homeassistant/components/aranet/ @aschmitz @thecode @anrijs
|
||||
/tests/components/aranet/ @aschmitz @thecode @anrijs
|
||||
/homeassistant/components/arcam_fmj/ @elupus
|
||||
/tests/components/arcam_fmj/ @elupus
|
||||
/homeassistant/components/arris_tg2492lg/ @vanbalken
|
||||
@ -161,6 +163,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/awair/ @ahayworth @danielsjf
|
||||
/homeassistant/components/axis/ @Kane610
|
||||
/tests/components/axis/ @Kane610
|
||||
/homeassistant/components/azure_data_explorer/ @kaareseras
|
||||
/tests/components/azure_data_explorer/ @kaareseras
|
||||
/homeassistant/components/azure_devops/ @timmo001
|
||||
/tests/components/azure_devops/ @timmo001
|
||||
/homeassistant/components/azure_event_hub/ @eavanvalkenburg
|
||||
@ -338,8 +342,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/homeassistant/components/dsmr/ @Robbie1221 @frenck
|
||||
/tests/components/dsmr/ @Robbie1221 @frenck
|
||||
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox
|
||||
/tests/components/dsmr_reader/ @sorted-bits @glodenox
|
||||
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||
/homeassistant/components/duotecno/ @cereal2nd
|
||||
/tests/components/duotecno/ @cereal2nd
|
||||
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
|
||||
@ -550,14 +554,14 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/group/ @home-assistant/core
|
||||
/homeassistant/components/guardian/ @bachya
|
||||
/tests/components/guardian/ @bachya
|
||||
/homeassistant/components/habitica/ @ASMfreaK @leikoilja
|
||||
/tests/components/habitica/ @ASMfreaK @leikoilja
|
||||
/homeassistant/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r
|
||||
/tests/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r
|
||||
/homeassistant/components/hardkernel/ @home-assistant/core
|
||||
/tests/components/hardkernel/ @home-assistant/core
|
||||
/homeassistant/components/hardware/ @home-assistant/core
|
||||
/tests/components/hardware/ @home-assistant/core
|
||||
/homeassistant/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan
|
||||
/tests/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan
|
||||
/homeassistant/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
|
||||
/tests/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
|
||||
/homeassistant/components/hassio/ @home-assistant/supervisor
|
||||
/tests/components/hassio/ @home-assistant/supervisor
|
||||
/homeassistant/components/hdmi_cec/ @inytar
|
||||
@ -650,6 +654,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/image_upload/ @home-assistant/core
|
||||
/homeassistant/components/imap/ @jbouwh
|
||||
/tests/components/imap/ @jbouwh
|
||||
/homeassistant/components/imgw_pib/ @bieniu
|
||||
/tests/components/imgw_pib/ @bieniu
|
||||
/homeassistant/components/improv_ble/ @emontnemery
|
||||
/tests/components/improv_ble/ @emontnemery
|
||||
/homeassistant/components/incomfort/ @zxdavb
|
||||
@ -690,6 +696,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/iqvia/ @bachya
|
||||
/tests/components/iqvia/ @bachya
|
||||
/homeassistant/components/irish_rail_transport/ @ttroy50
|
||||
/homeassistant/components/isal/ @bdraco
|
||||
/tests/components/isal/ @bdraco
|
||||
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||
/homeassistant/components/iss/ @DurgNomis-drol
|
||||
@ -865,6 +873,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
||||
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
||||
/tests/components/monoprice/ @etsinko @OnFreund
|
||||
/homeassistant/components/monzo/ @jakemartin-icl
|
||||
/tests/components/monzo/ @jakemartin-icl
|
||||
/homeassistant/components/moon/ @fabaff @frenck
|
||||
/tests/components/moon/ @fabaff @frenck
|
||||
/homeassistant/components/mopeka/ @bdraco
|
||||
@ -1271,8 +1281,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/smappee/ @bsmappee
|
||||
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
||||
/tests/components/smart_meter_texas/ @grahamwetzler
|
||||
/homeassistant/components/smartthings/ @andrewsayre
|
||||
/tests/components/smartthings/ @andrewsayre
|
||||
/homeassistant/components/smarttub/ @mdz
|
||||
/tests/components/smarttub/ @mdz
|
||||
/homeassistant/components/smarty/ @z0mbieprocess
|
||||
@ -1359,8 +1367,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/switchbee/ @jafar-atili
|
||||
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||
/homeassistant/components/switchbot_cloud/ @SeraphicRav
|
||||
/tests/components/switchbot_cloud/ @SeraphicRav
|
||||
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland
|
||||
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland
|
||||
/homeassistant/components/switcher_kis/ @thecode
|
||||
/tests/components/switcher_kis/ @thecode
|
||||
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
|
||||
@ -1413,7 +1421,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/thermobeacon/ @bdraco
|
||||
/homeassistant/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
|
||||
/tests/components/thread/ @home-assistant/core
|
||||
/homeassistant/components/tibber/ @danielhiversen
|
||||
@ -1477,8 +1486,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/unifi/ @Kane610
|
||||
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
||||
/homeassistant/components/unifiled/ @florisvdk
|
||||
/homeassistant/components/unifiprotect/ @AngellusMortis @bdraco
|
||||
/tests/components/unifiprotect/ @AngellusMortis @bdraco
|
||||
/homeassistant/components/unifiprotect/ @bdraco
|
||||
/tests/components/unifiprotect/ @bdraco
|
||||
/homeassistant/components/upb/ @gwww
|
||||
/tests/components/upb/ @gwww
|
||||
/homeassistant/components/upc_connect/ @pvizeli @fabaff
|
||||
|
@ -5,7 +5,7 @@
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
identity and expression, level of experience, education, socioeconomic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
|
@ -12,7 +12,7 @@ ENV \
|
||||
ARG QEMU_CPU
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.1.35
|
||||
RUN pip3 install uv==0.1.43
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
@ -35,21 +35,30 @@ RUN \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
# Setup hass-release
|
||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
||||
&& 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
|
||||
COPY requirements.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 ./
|
||||
RUN pip3 install -r requirements_test.txt
|
||||
RUN rm -rf requirements.txt requirements_test.txt requirements_test_pre_commit.txt homeassistant/
|
||||
RUN uv pip install -r requirements_test.txt
|
||||
|
||||
WORKDIR /workspaces
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
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/>`__,
|
||||
`tutorials <https://home-assistant.io/getting-started/automation/>`__ and `documentation <https://home-assistant.io/docs/>`__.
|
||||
|
||||
This is a project of the `Open Home Foundation <https://www.openhomefoundation.org/>`__.
|
||||
|
||||
|screenshot-states|
|
||||
|
||||
Featured integrations
|
||||
|
@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from contextlib import suppress
|
||||
import faulthandler
|
||||
import os
|
||||
import sys
|
||||
@ -208,8 +209,10 @@ def main() -> int:
|
||||
exit_code = runner.run(runtime_conf)
|
||||
faulthandler.disable()
|
||||
|
||||
if os.path.getsize(fault_file_name) == 0:
|
||||
os.remove(fault_file_name)
|
||||
# It's possible for the fault file to disappear, so suppress obvious errors
|
||||
with suppress(FileNotFoundError):
|
||||
if os.path.getsize(fault_file_name) == 0:
|
||||
os.remove(fault_file_name)
|
||||
|
||||
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 .models import AuthFlowResult
|
||||
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
||||
from .session import SessionManager
|
||||
|
||||
EVENT_USER_ADDED = "user_added"
|
||||
EVENT_USER_UPDATED = "user_updated"
|
||||
EVENT_USER_REMOVED = "user_removed"
|
||||
|
||||
_MfaModuleDict = dict[str, MultiFactorAuthModule]
|
||||
_ProviderKey = tuple[str, str | None]
|
||||
_ProviderDict = dict[_ProviderKey, AuthProvider]
|
||||
type _MfaModuleDict = dict[str, MultiFactorAuthModule]
|
||||
type _ProviderKey = tuple[str, str | None]
|
||||
type _ProviderDict = dict[_ProviderKey, AuthProvider]
|
||||
|
||||
|
||||
class InvalidAuthError(Exception):
|
||||
@ -181,7 +180,6 @@ class AuthManager:
|
||||
self._remove_expired_job = HassJob(
|
||||
self._async_remove_expired_refresh_tokens, job_type=HassJobType.Callback
|
||||
)
|
||||
self.session = SessionManager(hass, self)
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the auth manager."""
|
||||
@ -192,7 +190,6 @@ class AuthManager:
|
||||
)
|
||||
)
|
||||
self._async_track_next_refresh_token_expiration()
|
||||
await self.session.async_setup()
|
||||
|
||||
@property
|
||||
def auth_providers(self) -> list[AuthProvider]:
|
||||
@ -519,6 +516,13 @@ class AuthManager:
|
||||
for revoke_callback in callbacks:
|
||||
revoke_callback()
|
||||
|
||||
@callback
|
||||
def async_set_expiry(
|
||||
self, refresh_token: models.RefreshToken, *, enable_expiry: bool
|
||||
) -> None:
|
||||
"""Enable or disable expiry of a refresh token."""
|
||||
self._store.async_set_expiry(refresh_token, enable_expiry=enable_expiry)
|
||||
|
||||
@callback
|
||||
def _async_remove_expired_refresh_tokens(self, _: datetime | None = None) -> None:
|
||||
"""Remove expired refresh tokens."""
|
||||
|
@ -62,6 +62,7 @@ class AuthStore:
|
||||
self._store = Store[dict[str, list[dict[str, Any]]]](
|
||||
hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
|
||||
)
|
||||
self._token_id_to_user_id: dict[str, str] = {}
|
||||
|
||||
async def async_get_groups(self) -> list[models.Group]:
|
||||
"""Retrieve all users."""
|
||||
@ -135,7 +136,10 @@ class AuthStore:
|
||||
|
||||
async def async_remove_user(self, user: models.User) -> None:
|
||||
"""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()
|
||||
|
||||
async def async_update_user(
|
||||
@ -218,7 +222,9 @@ class AuthStore:
|
||||
kwargs["client_icon"] = client_icon
|
||||
|
||||
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()
|
||||
return refresh_token
|
||||
@ -226,19 +232,17 @@ class AuthStore:
|
||||
@callback
|
||||
def async_remove_refresh_token(self, refresh_token: models.RefreshToken) -> None:
|
||||
"""Remove a refresh token."""
|
||||
for user in self._users.values():
|
||||
if user.refresh_tokens.pop(refresh_token.id, None):
|
||||
self._async_schedule_save()
|
||||
break
|
||||
refresh_token_id = refresh_token.id
|
||||
if user_id := self._token_id_to_user_id.get(refresh_token_id):
|
||||
del self._users[user_id].refresh_tokens[refresh_token_id]
|
||||
del self._token_id_to_user_id[refresh_token_id]
|
||||
self._async_schedule_save()
|
||||
|
||||
@callback
|
||||
def async_get_refresh_token(self, token_id: str) -> models.RefreshToken | None:
|
||||
"""Get refresh token by id."""
|
||||
for user in self._users.values():
|
||||
refresh_token = user.refresh_tokens.get(token_id)
|
||||
if refresh_token is not None:
|
||||
return refresh_token
|
||||
|
||||
if user_id := self._token_id_to_user_id.get(token_id):
|
||||
return self._users[user_id].refresh_tokens.get(token_id)
|
||||
return None
|
||||
|
||||
@callback
|
||||
@ -277,6 +281,21 @@ class AuthStore:
|
||||
)
|
||||
self._async_schedule_save()
|
||||
|
||||
@callback
|
||||
def async_set_expiry(
|
||||
self, refresh_token: models.RefreshToken, *, enable_expiry: bool
|
||||
) -> None:
|
||||
"""Enable or disable expiry of a refresh token."""
|
||||
if enable_expiry:
|
||||
if refresh_token.expire_at is None:
|
||||
refresh_token.expire_at = (
|
||||
refresh_token.last_used_at or dt_util.utcnow()
|
||||
).timestamp() + REFRESH_TOKEN_EXPIRATION
|
||||
self._async_schedule_save()
|
||||
else:
|
||||
refresh_token.expire_at = None
|
||||
self._async_schedule_save()
|
||||
|
||||
async def async_load(self) -> None: # noqa: C901
|
||||
"""Load the users."""
|
||||
if self._loaded:
|
||||
@ -290,8 +309,6 @@ class AuthStore:
|
||||
perm_lookup = PermissionLookup(ent_reg, dev_reg)
|
||||
self._perm_lookup = perm_lookup
|
||||
|
||||
now_ts = dt_util.utcnow().timestamp()
|
||||
|
||||
if data is None or not isinstance(data, dict):
|
||||
self._set_defaults()
|
||||
return
|
||||
@ -445,14 +462,6 @@ class AuthStore:
|
||||
else:
|
||||
last_used_at = None
|
||||
|
||||
if (
|
||||
expire_at := rt_dict.get("expire_at")
|
||||
) is None and token_type == models.TOKEN_TYPE_NORMAL:
|
||||
if last_used_at:
|
||||
expire_at = last_used_at.timestamp() + REFRESH_TOKEN_EXPIRATION
|
||||
else:
|
||||
expire_at = now_ts + REFRESH_TOKEN_EXPIRATION
|
||||
|
||||
token = models.RefreshToken(
|
||||
id=rt_dict["id"],
|
||||
user=users[rt_dict["user_id"]],
|
||||
@ -469,7 +478,7 @@ class AuthStore:
|
||||
jwt_key=rt_dict["jwt_key"],
|
||||
last_used_at=last_used_at,
|
||||
last_used_ip=rt_dict.get("last_used_ip"),
|
||||
expire_at=expire_at,
|
||||
expire_at=rt_dict.get("expire_at"),
|
||||
version=rt_dict.get("version"),
|
||||
)
|
||||
if "credential_id" in rt_dict:
|
||||
@ -478,9 +487,18 @@ class AuthStore:
|
||||
|
||||
self._groups = groups
|
||||
self._users = users
|
||||
|
||||
self._build_token_id_to_user_id()
|
||||
self._async_schedule_save(INITIAL_LOAD_SAVE_DELAY)
|
||||
|
||||
@callback
|
||||
def _build_token_id_to_user_id(self) -> None:
|
||||
"""Build a map of token id to user id."""
|
||||
self._token_id_to_user_id = {
|
||||
token_id: user_id
|
||||
for user_id, user in self._users.items()
|
||||
for token_id in user.refresh_tokens
|
||||
}
|
||||
|
||||
@callback
|
||||
def _async_schedule_save(self, delay: float = DEFAULT_SAVE_DELAY) -> None:
|
||||
"""Save users."""
|
||||
@ -574,6 +592,7 @@ class AuthStore:
|
||||
read_only_group = _system_read_only_group()
|
||||
groups[read_only_group.id] = read_only_group
|
||||
self._groups = groups
|
||||
self._build_token_id_to_user_id()
|
||||
|
||||
|
||||
def _system_admin_group() -> models.Group:
|
||||
|
@ -16,6 +16,7 @@ from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.importlib import async_import_module
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry()
|
||||
|
||||
@ -29,7 +30,7 @@ MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema(
|
||||
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__)
|
||||
|
||||
|
@ -88,7 +88,7 @@ class NotifySetting:
|
||||
target: str | None = attr.ib(default=None)
|
||||
|
||||
|
||||
_UsersDict = dict[str, NotifySetting]
|
||||
type _UsersDict = dict[str, NotifySetting]
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
ValueType = (
|
||||
type ValueType = (
|
||||
# Example: entities.all = { read: true, control: true }
|
||||
Mapping[str, bool] | bool | None
|
||||
)
|
||||
|
||||
# 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
|
||||
Mapping[str, SubCategoryType]
|
||||
# Example: entities.all
|
||||
@ -24,4 +24,4 @@ CategoryType = (
|
||||
)
|
||||
|
||||
# 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 .types import CategoryType, SubCategoryDict, ValueType
|
||||
|
||||
LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], ValueType | None]
|
||||
SubCatLookupType = dict[str, LookupFunc]
|
||||
type LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], ValueType | None]
|
||||
type SubCatLookupType = dict[str, LookupFunc]
|
||||
|
||||
|
||||
def lookup_all(
|
||||
|
@ -17,13 +17,14 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.importlib import async_import_module
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from ..auth_store import AuthStore
|
||||
from ..const import MFA_SESSION_EXPIRATION
|
||||
from ..models import AuthFlowResult, Credentials, RefreshToken, User, UserMeta
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_REQS = "auth_prov_reqs_processed"
|
||||
DATA_REQS: HassKey[set[str]] = HassKey("auth_prov_reqs_processed")
|
||||
|
||||
AUTH_PROVIDERS: Registry[str, type[AuthProvider]] = Registry()
|
||||
|
||||
|
@ -28,8 +28,8 @@ from .. import InvalidAuthError
|
||||
from ..models import AuthFlowResult, Credentials, RefreshToken, UserMeta
|
||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||
|
||||
IPAddress = IPv4Address | IPv6Address
|
||||
IPNetwork = IPv4Network | IPv6Network
|
||||
type IPAddress = IPv4Address | IPv6Address
|
||||
type IPNetwork = IPv4Network | IPv6Network
|
||||
|
||||
CONF_TRUSTED_NETWORKS = "trusted_networks"
|
||||
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."""
|
||||
|
||||
import builtins
|
||||
from contextlib import suppress
|
||||
from http.client import HTTPConnection
|
||||
import importlib
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
@ -12,12 +14,21 @@ from .util.loop import protect_loop
|
||||
|
||||
_IN_TESTS = "unittest" in sys.modules
|
||||
|
||||
ALLOWED_FILE_PREFIXES = ("/proc",)
|
||||
|
||||
|
||||
def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
||||
# If the module is already imported, we can ignore it.
|
||||
return bool((args := mapped_args.get("args")) and args[0] in sys.modules)
|
||||
|
||||
|
||||
def _check_file_allowed(mapped_args: dict[str, Any]) -> bool:
|
||||
# If the file is in /proc we can ignore it.
|
||||
args = mapped_args["args"]
|
||||
path = args[0] if type(args[0]) is str else str(args[0]) # noqa: E721
|
||||
return path.startswith(ALLOWED_FILE_PREFIXES)
|
||||
|
||||
|
||||
def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
||||
#
|
||||
# Avoid extracting the stack unless we need to since it
|
||||
@ -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.
|
||||
#
|
||||
# frame[0] is us
|
||||
# frame[1] is check_loop
|
||||
# frame[1] is raise_for_blocking_call
|
||||
# frame[2] is protected_loop_func
|
||||
# frame[3] is the offender
|
||||
with suppress(ValueError):
|
||||
@ -35,21 +46,29 @@ def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
||||
|
||||
def enable() -> None:
|
||||
"""Enable the detection of blocking calls in the event loop."""
|
||||
loop_thread_id = threading.get_ident()
|
||||
# Prevent urllib3 and requests doing I/O in event loop
|
||||
HTTPConnection.putrequest = protect_loop( # type: ignore[method-assign]
|
||||
HTTPConnection.putrequest
|
||||
HTTPConnection.putrequest, loop_thread_id=loop_thread_id
|
||||
)
|
||||
|
||||
# Prevent sleeping in event loop. Non-strict since 2022.02
|
||||
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:
|
||||
# Prevent files being opened inside the event loop
|
||||
builtins.open = protect_loop( # type: ignore[assignment]
|
||||
builtins.open,
|
||||
strict_core=False,
|
||||
strict=False,
|
||||
check_allowed=_check_file_allowed,
|
||||
loop_thread_id=loop_thread_id,
|
||||
)
|
||||
# unittest uses `importlib.import_module` to do mocking
|
||||
# so we cannot protect it if we are running tests
|
||||
importlib.import_module = protect_loop(
|
||||
@ -57,4 +76,5 @@ def enable() -> None:
|
||||
strict_core=False,
|
||||
strict=False,
|
||||
check_allowed=_check_import_call_allowed,
|
||||
loop_thread_id=loop_thread_id,
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ from functools import partial
|
||||
from itertools import chain
|
||||
import logging
|
||||
import logging.handlers
|
||||
import mimetypes
|
||||
from operator import contains, itemgetter
|
||||
import os
|
||||
import platform
|
||||
@ -62,6 +63,7 @@ from .components import (
|
||||
)
|
||||
from .components.sensor import recorder as sensor_recorder # noqa: F401
|
||||
from .const import (
|
||||
BASE_PLATFORMS,
|
||||
FORMAT_DATETIME,
|
||||
KEY_DATA_LOGGING as DATA_LOGGING,
|
||||
REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
||||
@ -84,19 +86,23 @@ from .helpers import (
|
||||
template,
|
||||
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.system_info import async_get_system_info
|
||||
from .helpers.typing import ConfigType
|
||||
from .setup import (
|
||||
BASE_PLATFORMS,
|
||||
DATA_SETUP_STARTED,
|
||||
# _setup_started is marked as protected to make it clear
|
||||
# that it is not part of the public API and should not be used
|
||||
# by integrations. It is only used for internal tracking of
|
||||
# which integrations are being set up.
|
||||
_setup_started,
|
||||
async_get_setup_timings,
|
||||
async_notify_setup_error,
|
||||
async_set_domains_to_be_loaded,
|
||||
async_setup_component,
|
||||
)
|
||||
from .util.async_ import create_eager_task
|
||||
from .util.hass_dict import HassKey
|
||||
from .util.logging import async_activate_log_queue_handler
|
||||
from .util.package import async_get_user_site, is_virtual_env
|
||||
|
||||
@ -116,7 +122,7 @@ SETUP_ORDER_SORT_KEY = partial(contains, BASE_PLATFORMS)
|
||||
ERROR_LOG_FILENAME = "home-assistant.log"
|
||||
|
||||
# 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
|
||||
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:
|
||||
"""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:
|
||||
return
|
||||
hass.data[DATA_REGISTRIES_LOADED] = None
|
||||
|
||||
def _cache_uname_processor() -> None:
|
||||
"""Cache the result of platform.uname().processor in the executor.
|
||||
|
||||
Multiple modules call this function at startup which
|
||||
executes a blocking subprocess call. This is a problem for the
|
||||
asyncio event loop. By primeing the cache of uname we can
|
||||
avoid the blocking call in the event loop.
|
||||
"""
|
||||
_ = platform.uname().processor
|
||||
|
||||
# Load the registries and cache the result of platform.uname().processor
|
||||
translation.async_setup(hass)
|
||||
entity.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(issue_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(restore_state.async_load(hass)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
@ -425,7 +432,11 @@ async def async_from_config_dict(
|
||||
if not all(
|
||||
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
|
||||
)
|
||||
)
|
||||
@ -679,7 +690,7 @@ class _WatchPendingSetups:
|
||||
|
||||
if 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)
|
||||
self._async_dispatch(remaining_with_setup_started)
|
||||
if (
|
||||
@ -699,7 +710,7 @@ class _WatchPendingSetups:
|
||||
def _async_dispatch(self, remaining_with_setup_started: dict[str, float]) -> None:
|
||||
"""Dispatch the signal."""
|
||||
if remaining_with_setup_started or not self._previous_was_empty:
|
||||
async_dispatcher_send(
|
||||
async_dispatcher_send_internal(
|
||||
self._hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, 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]
|
||||
) -> None:
|
||||
"""Set up all the integrations."""
|
||||
setup_started: dict[tuple[str, str | None], float] = {}
|
||||
hass.data[DATA_SETUP_STARTED] = setup_started
|
||||
watcher = _WatchPendingSetups(hass, setup_started)
|
||||
watcher = _WatchPendingSetups(hass, _setup_started(hass))
|
||||
watcher.async_start()
|
||||
|
||||
domains_to_setup, integration_cache = await _async_resolve_domains_to_setup(
|
||||
@ -985,7 +994,7 @@ async def _async_set_up_integrations(
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"Setup timed out for stage 1 waiting on %s - moving forward",
|
||||
hass._active_tasks, # pylint: disable=protected-access
|
||||
hass._active_tasks, # noqa: SLF001
|
||||
)
|
||||
|
||||
# Add after dependencies when setting up stage 2 domains
|
||||
@ -1001,7 +1010,7 @@ async def _async_set_up_integrations(
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"Setup timed out for stage 2 waiting on %s - moving forward",
|
||||
hass._active_tasks, # pylint: disable=protected-access
|
||||
hass._active_tasks, # noqa: SLF001
|
||||
)
|
||||
|
||||
# Wrap up startup
|
||||
@ -1012,7 +1021,7 @@ async def _async_set_up_integrations(
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"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()
|
||||
|
@ -5,9 +5,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
|
||||
from jaraco.abode.automation import Automation as AbodeAuto
|
||||
from jaraco.abode.client import Client as Abode
|
||||
from jaraco.abode.devices.base import Device as AbodeDev
|
||||
from jaraco.abode.exceptions import (
|
||||
AuthenticationException as AbodeAuthenticationException,
|
||||
Exception as AbodeException,
|
||||
@ -29,11 +27,11 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
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_CAPTURE_IMAGE = "capture_image"
|
||||
@ -83,6 +81,12 @@ class AbodeSystem:
|
||||
logout_listener: CALLBACK_TYPE | None = None
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Abode component."""
|
||||
setup_hass_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Abode integration from a config entry."""
|
||||
username = entry.data[CONF_USERNAME]
|
||||
@ -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 setup_hass_events(hass)
|
||||
await hass.async_add_executor_job(setup_hass_services, hass)
|
||||
await hass.async_add_executor_job(setup_abode_events, hass)
|
||||
|
||||
return True
|
||||
@ -119,10 +122,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
hass.services.async_remove(DOMAIN, SERVICE_SETTINGS)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_TRIGGER_AUTOMATION)
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop)
|
||||
@ -175,15 +174,15 @@ def setup_hass_services(hass: HomeAssistant) -> None:
|
||||
signal = f"abode_trigger_automation_{entity_id}"
|
||||
dispatcher_send(hass, signal)
|
||||
|
||||
hass.services.register(
|
||||
hass.services.async_register(
|
||||
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
|
||||
)
|
||||
|
||||
hass.services.register(
|
||||
hass.services.async_register(
|
||||
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(
|
||||
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.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@ -22,8 +22,9 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
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.util import Throttle
|
||||
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .entity import AbodeDevice
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
|
||||
|
@ -10,8 +10,9 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@ -10,8 +10,9 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@ -27,8 +27,9 @@ from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AbodeDevice, AbodeSystem
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeDevice
|
||||
|
||||
ABODE_TEMPERATURE_UNIT_HA_UNIT = {
|
||||
UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,
|
||||
|
@ -13,8 +13,9 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AbodeAutomation, AbodeDevice, AbodeSystem
|
||||
from . import AbodeSystem
|
||||
from .const import DOMAIN
|
||||
from .entity import AbodeAutomation, AbodeDevice
|
||||
|
||||
DEVICE_TYPES = [TYPE_SWITCH, TYPE_VALVE]
|
||||
|
||||
|
@ -33,7 +33,10 @@ class AccuWeatherData:
|
||||
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."""
|
||||
api_key: str = entry.data[CONF_API_KEY]
|
||||
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_daily_forecast.async_config_entry_first_refresh()
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AccuWeatherData(
|
||||
entry.runtime_data = AccuWeatherData(
|
||||
coordinator_observation=coordinator_observation,
|
||||
coordinator_daily_forecast=coordinator_daily_forecast,
|
||||
)
|
||||
@ -84,16 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
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_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
@ -5,21 +5,19 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AccuWeatherData
|
||||
from .const import DOMAIN
|
||||
from . import AccuWeatherConfigEntry, AccuWeatherData
|
||||
|
||||
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: AccuWeatherConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
accuweather_data: AccuWeatherData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
accuweather_data: AccuWeatherData = config_entry.runtime_data
|
||||
|
||||
return {
|
||||
"config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT),
|
||||
|
@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
PERCENTAGE,
|
||||
@ -28,7 +27,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AccuWeatherData
|
||||
from . import AccuWeatherConfigEntry
|
||||
from .const import (
|
||||
API_METRIC,
|
||||
ATTR_CATEGORY,
|
||||
@ -38,7 +37,6 @@ from .const import (
|
||||
ATTR_SPEED,
|
||||
ATTR_VALUE,
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
MAX_FORECAST_DAYS,
|
||||
)
|
||||
from .coordinator import (
|
||||
@ -458,17 +456,16 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: AccuWeatherConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add AccuWeather entities from a config_entry."""
|
||||
|
||||
accuweather_data: AccuWeatherData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
observation_coordinator: AccuWeatherObservationDataUpdateCoordinator = (
|
||||
accuweather_data.coordinator_observation
|
||||
entry.runtime_data.coordinator_observation
|
||||
)
|
||||
forecast_daily_coordinator: AccuWeatherDailyForecastDataUpdateCoordinator = (
|
||||
accuweather_data.coordinator_daily_forecast
|
||||
entry.runtime_data.coordinator_daily_forecast
|
||||
)
|
||||
|
||||
sensors: list[AccuWeatherSensor | AccuWeatherForecastSensor] = [
|
||||
|
@ -9,6 +9,7 @@ from accuweather.const import ENDPOINT
|
||||
from homeassistant.components import system_health
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import AccuWeatherConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@ -22,9 +23,11 @@ def async_register(
|
||||
|
||||
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"""Get info for the info page."""
|
||||
remaining_requests = list(hass.data[DOMAIN].values())[
|
||||
0
|
||||
].coordinator_observation.accuweather.requests_remaining
|
||||
config_entry: AccuWeatherConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
|
||||
remaining_requests = (
|
||||
config_entry.runtime_data.coordinator_observation.accuweather.requests_remaining
|
||||
)
|
||||
|
||||
return {
|
||||
"can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT),
|
||||
|
@ -21,7 +21,6 @@ from homeassistant.components.weather import (
|
||||
Forecast,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
UnitOfLength,
|
||||
UnitOfPrecipitationDepth,
|
||||
@ -31,10 +30,9 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
|
||||
from homeassistant.util.dt import utc_from_timestamp
|
||||
|
||||
from . import AccuWeatherData
|
||||
from . import AccuWeatherConfigEntry, AccuWeatherData
|
||||
from .const import (
|
||||
API_METRIC,
|
||||
ATTR_DIRECTION,
|
||||
@ -42,7 +40,6 @@ from .const import (
|
||||
ATTR_VALUE,
|
||||
ATTRIBUTION,
|
||||
CONDITION_MAP,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import (
|
||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||
@ -53,20 +50,18 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: AccuWeatherConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add a AccuWeather weather entity from a config_entry."""
|
||||
accuweather_data: AccuWeatherData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities([AccuWeatherEntity(accuweather_data)])
|
||||
async_add_entities([AccuWeatherEntity(entry.runtime_data)])
|
||||
|
||||
|
||||
class AccuWeatherEntity(
|
||||
CoordinatorWeatherEntity[
|
||||
AccuWeatherObservationDataUpdateCoordinator,
|
||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||
TimestampDataUpdateCoordinator,
|
||||
TimestampDataUpdateCoordinator,
|
||||
]
|
||||
):
|
||||
"""Define an AccuWeather entity."""
|
||||
|
@ -4,30 +4,35 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .hub import PulseHub
|
||||
|
||||
CONF_HUBS = "hubs"
|
||||
|
||||
PLATFORMS = [Platform.COVER, Platform.SENSOR]
|
||||
|
||||
type AcmedaConfigEntry = ConfigEntry[PulseHub]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: AcmedaConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Rollease Acmeda Automate hub from a config entry."""
|
||||
hub = PulseHub(hass, config_entry)
|
||||
|
||||
if not await hub.async_setup():
|
||||
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)
|
||||
|
||||
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."""
|
||||
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||
hub = config_entry.runtime_data
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
@ -36,7 +41,4 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
if not await hub.async_reset():
|
||||
return False
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
@ -9,24 +9,23 @@ from homeassistant.components.cover import (
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AcmedaConfigEntry
|
||||
from .base import AcmedaBase
|
||||
from .const import ACMEDA_HUB_UPDATE, DOMAIN
|
||||
from .const import ACMEDA_HUB_UPDATE
|
||||
from .helpers import async_add_acmeda_entities
|
||||
from .hub import PulseHub
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AcmedaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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()
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiopulse import Roller
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -11,17 +13,20 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AcmedaConfigEntry
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_acmeda_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_class: type,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AcmedaConfigEntry,
|
||||
current: set[int],
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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)
|
||||
|
||||
api = hub.api.rollers
|
||||
|
@ -3,25 +3,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AcmedaConfigEntry
|
||||
from .base import AcmedaBase
|
||||
from .const import ACMEDA_HUB_UPDATE, DOMAIN
|
||||
from .const import ACMEDA_HUB_UPDATE
|
||||
from .helpers import async_add_acmeda_entities
|
||||
from .hub import PulseHub
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AcmedaConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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()
|
||||
|
||||
|
@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
@ -43,6 +43,7 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
||||
type AdGuardConfigEntry = ConfigEntry[AdGuardData]
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -53,7 +54,7 @@ class AdGuardData:
|
||||
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."""
|
||||
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
|
||||
adguard = AdGuardHome(
|
||||
@ -71,7 +72,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except AdGuardHomeConnectionError as 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)
|
||||
|
||||
@ -116,17 +117,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
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_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if not hass.data[DOMAIN]:
|
||||
loaded_entries = [
|
||||
entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.state == ConfigEntryState.LOADED
|
||||
]
|
||||
if len(loaded_entries) == 1:
|
||||
# This is the last loaded instance of AdGuard, deregister any services
|
||||
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
|
||||
del hass.data[DOMAIN]
|
||||
|
||||
return unload_ok
|
||||
|
@ -4,11 +4,11 @@ from __future__ import annotations
|
||||
|
||||
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.entity import Entity
|
||||
|
||||
from . import AdGuardData
|
||||
from . import AdGuardConfigEntry, AdGuardData
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ class AdGuardHomeEntity(Entity):
|
||||
def __init__(
|
||||
self,
|
||||
data: AdGuardData,
|
||||
entry: ConfigEntry,
|
||||
entry: AdGuardConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the AdGuard Home entity."""
|
||||
self._entry = entry
|
||||
|
@ -10,12 +10,11 @@ from typing import Any
|
||||
from adguardhome import AdGuardHome
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdGuardData
|
||||
from . import AdGuardConfigEntry, AdGuardData
|
||||
from .const import DOMAIN
|
||||
from .entity import AdGuardHomeEntity
|
||||
|
||||
@ -85,11 +84,11 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AdGuardConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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(
|
||||
[AdGuardHomeSensor(data, entry, description) for description in SENSORS],
|
||||
@ -105,7 +104,7 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
data: AdGuardData,
|
||||
entry: ConfigEntry,
|
||||
entry: AdGuardConfigEntry,
|
||||
description: AdGuardHomeEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize AdGuard Home sensor."""
|
||||
|
@ -10,11 +10,10 @@ from typing import Any
|
||||
from adguardhome import AdGuardHome, AdGuardHomeError
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdGuardData
|
||||
from . import AdGuardConfigEntry, AdGuardData
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .entity import AdGuardHomeEntity
|
||||
|
||||
@ -79,11 +78,11 @@ SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: AdGuardConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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(
|
||||
[AdGuardHomeSwitch(data, entry, description) for description in SWITCHES],
|
||||
@ -99,7 +98,7 @@ class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity):
|
||||
def __init__(
|
||||
self,
|
||||
data: AdGuardData,
|
||||
entry: ConfigEntry,
|
||||
entry: AdGuardConfigEntry,
|
||||
description: AdGuardHomeSwitchEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize AdGuard Home switch."""
|
||||
|
@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ads",
|
||||
"iot_class": "local_push",
|
||||
"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.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import ADVANTAGE_AIR_RETRY, DOMAIN
|
||||
from .const import ADVANTAGE_AIR_RETRY
|
||||
from .models import AdvantageAirData
|
||||
|
||||
type AdvantageAirDataConfigEntry = ConfigEntry[AdvantageAirData]
|
||||
|
||||
ADVANTAGE_AIR_SYNC_INTERVAL = 15
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@ -31,7 +33,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
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."""
|
||||
ip_address = entry.data[CONF_IP_ADDRESS]
|
||||
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()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = AdvantageAirData(coordinator, api)
|
||||
entry.runtime_data = AdvantageAirData(coordinator, api)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
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_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
@ -6,12 +6,11 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
|
||||
from .models import AdvantageAirData
|
||||
|
||||
@ -20,12 +19,12 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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] = []
|
||||
if aircons := instance.coordinator.data.get("aircons"):
|
||||
|
@ -16,19 +16,18 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import (
|
||||
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
||||
ADVANTAGE_AIR_STATE_CLOSE,
|
||||
ADVANTAGE_AIR_STATE_OFF,
|
||||
ADVANTAGE_AIR_STATE_ON,
|
||||
ADVANTAGE_AIR_STATE_OPEN,
|
||||
DOMAIN as ADVANTAGE_AIR_DOMAIN,
|
||||
)
|
||||
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
|
||||
from .models import AdvantageAirData
|
||||
@ -76,12 +75,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir climate platform."""
|
||||
|
||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
||||
instance = config_entry.runtime_data
|
||||
|
||||
entities: list[ClimateEntity] = []
|
||||
if aircons := instance.coordinator.data.get("aircons"):
|
||||
|
@ -8,15 +8,11 @@ from homeassistant.components.cover import (
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
ADVANTAGE_AIR_STATE_CLOSE,
|
||||
ADVANTAGE_AIR_STATE_OPEN,
|
||||
DOMAIN as ADVANTAGE_AIR_DOMAIN,
|
||||
)
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import ADVANTAGE_AIR_STATE_CLOSE, ADVANTAGE_AIR_STATE_OPEN
|
||||
from .entity import AdvantageAirThingEntity, AdvantageAirZoneEntity
|
||||
from .models import AdvantageAirData
|
||||
|
||||
@ -25,12 +21,12 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir cover platform."""
|
||||
|
||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
||||
instance = config_entry.runtime_data
|
||||
|
||||
entities: list[CoverEntity] = []
|
||||
if aircons := instance.coordinator.data.get("aircons"):
|
||||
|
@ -5,10 +5,9 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
|
||||
TO_REDACT = [
|
||||
"dealerPhoneNumber",
|
||||
@ -25,10 +24,10 @@ TO_REDACT = [
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: AdvantageAirDataConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""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 {
|
||||
|
@ -3,11 +3,11 @@
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
from .entity import AdvantageAirEntity, AdvantageAirThingEntity
|
||||
from .models import AdvantageAirData
|
||||
@ -15,12 +15,12 @@ from .models import AdvantageAirData
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir light platform."""
|
||||
|
||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
||||
instance = config_entry.runtime_data
|
||||
|
||||
entities: list[LightEntity] = []
|
||||
if my_lights := instance.coordinator.data.get("myLights"):
|
||||
|
@ -1,11 +1,10 @@
|
||||
"""Select platform for Advantage Air integration."""
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .entity import AdvantageAirAcEntity
|
||||
from .models import AdvantageAirData
|
||||
|
||||
@ -14,12 +13,12 @@ ADVANTAGE_AIR_INACTIVE = "Inactive"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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"):
|
||||
async_add_entities(AdvantageAirMyZone(instance, ac_key) for ac_key in aircons)
|
||||
|
@ -12,13 +12,13 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .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 .models import AdvantageAirData
|
||||
|
||||
@ -31,12 +31,12 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir sensor platform."""
|
||||
|
||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
||||
instance = config_entry.runtime_data
|
||||
|
||||
entities: list[SensorEntity] = []
|
||||
if aircons := instance.coordinator.data.get("aircons"):
|
||||
|
@ -3,15 +3,14 @@
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import (
|
||||
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
||||
ADVANTAGE_AIR_STATE_OFF,
|
||||
ADVANTAGE_AIR_STATE_ON,
|
||||
DOMAIN as ADVANTAGE_AIR_DOMAIN,
|
||||
)
|
||||
from .entity import AdvantageAirAcEntity, AdvantageAirThingEntity
|
||||
from .models import AdvantageAirData
|
||||
@ -19,12 +18,12 @@ from .models import AdvantageAirData
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AdvantageAir switch platform."""
|
||||
|
||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
||||
instance = config_entry.runtime_data
|
||||
|
||||
entities: list[SwitchEntity] = []
|
||||
if aircons := instance.coordinator.data.get("aircons"):
|
||||
|
@ -1,11 +1,11 @@
|
||||
"""Advantage Air Update platform."""
|
||||
|
||||
from homeassistant.components.update import UpdateEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AdvantageAirDataConfigEntry
|
||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
from .entity import AdvantageAirEntity
|
||||
from .models import AdvantageAirData
|
||||
@ -13,12 +13,12 @@ from .models import AdvantageAirData
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AdvantageAirDataConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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)])
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""The AEMET OpenData component."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from aemet_opendata.exceptions import AemetError, TownNotFound
|
||||
@ -11,19 +12,23 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import (
|
||||
CONF_STATION_UPDATES,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .const import CONF_STATION_UPDATES, PLATFORMS
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
|
||||
_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."""
|
||||
name = entry.data[CONF_NAME]
|
||||
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)
|
||||
await weather_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
ENTRY_NAME: name,
|
||||
ENTRY_WEATHER_COORDINATOR: weather_coordinator,
|
||||
}
|
||||
entry.runtime_data = AemetData(name=name, coordinator=weather_coordinator)
|
||||
|
||||
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:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
@ -55,8 +55,6 @@ CONF_STATION_UPDATES = "station_updates"
|
||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
DEFAULT_NAME = "AEMET"
|
||||
DOMAIN = "aemet"
|
||||
ENTRY_NAME = "name"
|
||||
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
|
||||
|
||||
ATTR_API_CONDITION = "condition"
|
||||
ATTR_API_FORECAST_CONDITION = "condition"
|
||||
|
@ -7,7 +7,6 @@ from typing import Any
|
||||
from aemet_opendata.const import AOD_COORDS
|
||||
|
||||
from homeassistant.components.diagnostics.util import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LATITUDE,
|
||||
@ -16,8 +15,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN, ENTRY_WEATHER_COORDINATOR
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
from . import AemetConfigEntry
|
||||
|
||||
TO_REDACT_CONFIG = [
|
||||
CONF_API_KEY,
|
||||
@ -32,11 +30,10 @@ TO_REDACT_COORD = [
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: AemetConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
aemet_entry = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator: WeatherUpdateCoordinator = aemet_entry[ENTRY_WEATHER_COORDINATOR]
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
return {
|
||||
"api_data": coordinator.aemet.raw_data(),
|
||||
|
@ -56,6 +56,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import AemetConfigEntry
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_FORECAST_CONDITION,
|
||||
@ -87,9 +88,6 @@ from .const import (
|
||||
ATTR_API_WIND_SPEED,
|
||||
ATTRIBUTION,
|
||||
CONDITIONS_MAP,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
)
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
from .entity import AemetEntity
|
||||
@ -360,13 +358,13 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AemetConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AEMET OpenData sensor entities based on a config entry."""
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
name: str = domain_data[ENTRY_NAME]
|
||||
coordinator: WeatherUpdateCoordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
domain_data = config_entry.runtime_data
|
||||
name = domain_data.name
|
||||
coordinator = domain_data.coordinator
|
||||
|
||||
async_add_entities(
|
||||
AemetSensor(
|
||||
|
@ -18,7 +18,6 @@ from homeassistant.components.weather import (
|
||||
SingleCoordinatorWeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfPressure,
|
||||
@ -28,32 +27,24 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
CONDITIONS_MAP,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
ENTRY_WEATHER_COORDINATOR,
|
||||
)
|
||||
from . import AemetConfigEntry
|
||||
from .const import ATTRIBUTION, CONDITIONS_MAP
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
from .entity import AemetEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AemetConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AEMET OpenData weather entity based on a config entry."""
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
||||
domain_data = config_entry.runtime_data
|
||||
name = domain_data.name
|
||||
weather_coordinator = domain_data.coordinator
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
AemetWeather(
|
||||
domain_data[ENTRY_NAME], config_entry.unique_id, weather_coordinator
|
||||
)
|
||||
],
|
||||
[AemetWeather(name, config_entry.unique_id, weather_coordinator)],
|
||||
False,
|
||||
)
|
||||
|
||||
|
@ -10,16 +10,14 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
type AfterShipConfigEntry = ConfigEntry[AfterShip]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AfterShipConfigEntry) -> bool:
|
||||
"""Set up AfterShip from a config entry."""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
aftership = AfterShip(api_key=entry.data[CONF_API_KEY], session=session)
|
||||
|
||||
@ -28,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except AfterShipException as 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)
|
||||
|
||||
@ -37,7 +35,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
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
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
@ -8,7 +8,6 @@ from typing import Any, Final
|
||||
from pyaftership import AfterShip, AfterShipException
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
@ -18,6 +17,7 @@ from homeassistant.helpers.dispatcher import (
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from . import AfterShipConfigEntry
|
||||
from .const import (
|
||||
ADD_TRACKING_SERVICE_SCHEMA,
|
||||
ATTR_TRACKINGS,
|
||||
@ -41,11 +41,11 @@ PLATFORM_SCHEMA: Final = cv.removed(DOMAIN, raise_if_present=False)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AfterShipConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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)
|
||||
|
||||
|
@ -10,18 +10,20 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONNECTION, DOMAIN as AGENT_DOMAIN, SERVER_URL
|
||||
from .const import DOMAIN as AGENT_DOMAIN, SERVER_URL
|
||||
|
||||
ATTRIBUTION = "ispyconnect.com"
|
||||
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
|
||||
|
||||
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
|
||||
|
||||
AgentDVRConfigEntry = ConfigEntry[Agent]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: AgentDVRConfigEntry
|
||||
) -> bool:
|
||||
"""Set up the Agent component."""
|
||||
hass.data.setdefault(AGENT_DOMAIN, {})
|
||||
|
||||
server_origin = config_entry.data[SERVER_URL]
|
||||
|
||||
agent_client = Agent(server_origin, async_get_clientsession(hass))
|
||||
@ -34,9 +36,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
if not agent_client.is_available:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
config_entry.async_on_unload(agent_client.close)
|
||||
|
||||
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)
|
||||
|
||||
@ -54,15 +58,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
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_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
|
||||
await hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION].close()
|
||||
|
||||
if unload_ok:
|
||||
hass.data[AGENT_DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
@ -6,7 +6,6 @@ from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
AlarmControlPanelEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
@ -17,7 +16,8 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
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_AWAY_MODE_NAME = "away"
|
||||
@ -28,13 +28,11 @@ CONST_ALARM_CONTROL_PANEL_NAME = "Alarm Panel"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AgentDVRConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Agent DVR Alarm Control Panels."""
|
||||
async_add_entities(
|
||||
[AgentBaseStation(hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION])]
|
||||
)
|
||||
async_add_entities([AgentBaseStation(config_entry.runtime_data)])
|
||||
|
||||
|
||||
class AgentBaseStation(AlarmControlPanelEntity):
|
||||
|
@ -7,7 +7,6 @@ from agent import AgentError
|
||||
|
||||
from homeassistant.components.camera import CameraEntityFeature
|
||||
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
@ -15,12 +14,8 @@ from homeassistant.helpers.entity_platform import (
|
||||
async_get_current_platform,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
CAMERA_SCAN_INTERVAL_SECS,
|
||||
CONNECTION,
|
||||
DOMAIN as AGENT_DOMAIN,
|
||||
)
|
||||
from . import AgentDVRConfigEntry
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
|
||||
|
||||
@ -43,14 +38,14 @@ CAMERA_SERVICES = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AgentDVRConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Agent cameras."""
|
||||
filter_urllib3_logging()
|
||||
cameras = []
|
||||
|
||||
server = hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION]
|
||||
server = config_entry.runtime_data
|
||||
if not server.devices:
|
||||
_LOGGER.warning("Could not fetch cameras from Agent server")
|
||||
return
|
||||
@ -80,11 +75,11 @@ class AgentCamera(MjpegCamera):
|
||||
"""Initialize as a subclass of MjpegCamera."""
|
||||
self.device = device
|
||||
self._removed = False
|
||||
self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}"
|
||||
self._attr_unique_id = f"{device.client.unique}_{device.typeID}_{device.id}"
|
||||
super().__init__(
|
||||
name=device.name,
|
||||
mjpeg_url=f"{device.client._server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}",
|
||||
still_image_url=f"{device.client._server_url}{device.still_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}", # noqa: SLF001
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(AGENT_DOMAIN, self.unique_id)},
|
||||
|
@ -9,4 +9,3 @@ SERVICE_UPDATE = "update"
|
||||
SIGNAL_UPDATE_AGENT = "agent_update"
|
||||
ATTRIBUTION = "Data provided by ispyconnect.com"
|
||||
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 . import group as group_pre_import # noqa: F401
|
||||
from .const import DOMAIN
|
||||
|
||||
_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_SO2: Final = "sulphur_dioxide"
|
||||
|
||||
DOMAIN: Final = "air_quality"
|
||||
|
||||
ENTITY_ID_FORMAT: Final = DOMAIN + ".{}"
|
||||
|
||||
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."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@ -7,10 +9,12 @@ from homeassistant.core import HomeAssistant, callback
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components.group import GroupIntegrationRegistry
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def async_describe_on_off_states(
|
||||
hass: HomeAssistant, registry: "GroupIntegrationRegistry"
|
||||
hass: HomeAssistant, registry: GroupIntegrationRegistry
|
||||
) -> None:
|
||||
"""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__)
|
||||
|
||||
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."""
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
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()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
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
|
||||
|
||||
|
||||
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_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
@ -55,7 +55,7 @@ def set_update_interval(instances_count: int, requests_remaining: int) -> timede
|
||||
return interval
|
||||
|
||||
|
||||
class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
class AirlyDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str | float | int]]):
|
||||
"""Define an object to hold Airly data."""
|
||||
|
||||
def __init__(
|
||||
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LATITUDE,
|
||||
@ -14,17 +13,16 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AirlyDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from . import AirlyConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIQUE_ID}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: AirlyConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: AirlyDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
return {
|
||||
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
|
||||
|
@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONF_NAME,
|
||||
@ -25,7 +24,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AirlyDataUpdateCoordinator
|
||||
from . import AirlyConfigEntry, AirlyDataUpdateCoordinator
|
||||
from .const import (
|
||||
ATTR_ADVICE,
|
||||
ATTR_API_ADVICE,
|
||||
@ -174,12 +173,14 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: AirlyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Airly sensor entities based on a config entry."""
|
||||
name = entry.data[CONF_NAME]
|
||||
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
(
|
||||
|
@ -9,6 +9,7 @@ from airly import Airly
|
||||
from homeassistant.components import system_health
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import AirlyConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@ -22,8 +23,10 @@ def async_register(
|
||||
|
||||
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
||||
"""Get info for the info page."""
|
||||
requests_remaining = list(hass.data[DOMAIN].values())[0].airly.requests_remaining
|
||||
requests_per_day = list(hass.data[DOMAIN].values())[0].airly.requests_per_day
|
||||
config_entry: AirlyConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
|
||||
requests_remaining = config_entry.runtime_data.airly.requests_remaining
|
||||
requests_per_day = config_entry.runtime_data.airly.requests_per_day
|
||||
|
||||
return {
|
||||
"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.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN # noqa: F401
|
||||
from .coordinator import AirNowDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
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."""
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
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()
|
||||
|
||||
# Store Entity and Initialize Platforms
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
# Listen for option changes
|
||||
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
|
||||
|
||||
|
||||
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_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
|
@ -82,7 +82,7 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "invalid_auth"
|
||||
except InvalidLocation:
|
||||
errors["base"] = "invalid_location"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
|
@ -8,6 +8,7 @@ ATTR_API_CATEGORY = "Category"
|
||||
ATTR_API_CAT_LEVEL = "Number"
|
||||
ATTR_API_CAT_DESCRIPTION = "Name"
|
||||
ATTR_API_O3 = "O3"
|
||||
ATTR_API_PM10 = "PM10"
|
||||
ATTR_API_PM25 = "PM2.5"
|
||||
ATTR_API_POLLUTANT = "Pollutant"
|
||||
ATTR_API_REPORT_DATE = "DateObserved"
|
||||
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LATITUDE,
|
||||
@ -14,8 +13,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AirNowDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from . import AirNowConfigEntry
|
||||
|
||||
ATTR_LATITUDE_CAP = "Latitude"
|
||||
ATTR_LONGITUDE_CAP = "Longitude"
|
||||
@ -40,10 +38,10 @@ TO_REDACT = {
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
hass: HomeAssistant, entry: AirNowConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: AirNowDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
return async_redact_data(
|
||||
{
|
||||
|
@ -4,6 +4,9 @@
|
||||
"aqi": {
|
||||
"default": "mdi:blur"
|
||||
},
|
||||
"pm10": {
|
||||
"default": "mdi:blur"
|
||||
},
|
||||
"pm25": {
|
||||
"default": "mdi:blur"
|
||||
},
|
||||
|
@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TIME,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
@ -26,12 +25,13 @@ from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.dt import get_time_zone
|
||||
|
||||
from . import AirNowDataUpdateCoordinator
|
||||
from . import AirNowConfigEntry, AirNowDataUpdateCoordinator
|
||||
from .const import (
|
||||
ATTR_API_AQI,
|
||||
ATTR_API_AQI_DESCRIPTION,
|
||||
ATTR_API_AQI_LEVEL,
|
||||
ATTR_API_O3,
|
||||
ATTR_API_PM10,
|
||||
ATTR_API_PM25,
|
||||
ATTR_API_REPORT_DATE,
|
||||
ATTR_API_REPORT_HOUR,
|
||||
@ -88,6 +88,15 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
|
||||
.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(
|
||||
key=ATTR_API_PM25,
|
||||
translation_key="pm25",
|
||||
@ -116,11 +125,11 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AirNowConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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]
|
||||
|
||||
|
@ -36,7 +36,7 @@
|
||||
"name": "[%key:component::sensor::entity_component::ozone::name%]"
|
||||
},
|
||||
"station": {
|
||||
"name": "PM2.5 reporting station",
|
||||
"name": "Reporting station",
|
||||
"state_attributes": {
|
||||
"lat": { "name": "[%key:common::config_flow::data::latitude%]" },
|
||||
"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