mirror of
https://github.com/home-assistant/core.git
synced 2025-08-02 18:18:21 +00:00
Merge branch 'dev' into block_pyserial_asyncio
This commit is contained in:
commit
5aacb6e1b8
@ -120,21 +120,18 @@ tests: &tests
|
|||||||
- pylint/**
|
- pylint/**
|
||||||
- requirements_test_pre_commit.txt
|
- requirements_test_pre_commit.txt
|
||||||
- requirements_test.txt
|
- requirements_test.txt
|
||||||
|
- tests/*.py
|
||||||
- tests/auth/**
|
- tests/auth/**
|
||||||
- tests/backports/**
|
- tests/backports/**
|
||||||
- tests/common.py
|
|
||||||
- tests/components/history/**
|
- tests/components/history/**
|
||||||
- tests/components/logbook/**
|
- tests/components/logbook/**
|
||||||
- tests/components/recorder/**
|
- tests/components/recorder/**
|
||||||
- tests/components/sensor/**
|
- tests/components/sensor/**
|
||||||
- tests/conftest.py
|
|
||||||
- tests/hassfest/**
|
- tests/hassfest/**
|
||||||
- tests/helpers/**
|
- tests/helpers/**
|
||||||
- tests/ignore_uncaught_exceptions.py
|
|
||||||
- tests/mock/**
|
- tests/mock/**
|
||||||
- tests/pylint/**
|
- tests/pylint/**
|
||||||
- tests/scripts/**
|
- tests/scripts/**
|
||||||
- tests/syrupy.py
|
|
||||||
- tests/test_util/**
|
- tests/test_util/**
|
||||||
- tests/testing_config/**
|
- tests/testing_config/**
|
||||||
- tests/util/**
|
- tests/util/**
|
||||||
|
121
.coveragerc
121
.coveragerc
@ -58,13 +58,18 @@ omit =
|
|||||||
homeassistant/components/airvisual/sensor.py
|
homeassistant/components/airvisual/sensor.py
|
||||||
homeassistant/components/airvisual_pro/__init__.py
|
homeassistant/components/airvisual_pro/__init__.py
|
||||||
homeassistant/components/airvisual_pro/sensor.py
|
homeassistant/components/airvisual_pro/sensor.py
|
||||||
|
homeassistant/components/aladdin_connect/__init__.py
|
||||||
|
homeassistant/components/aladdin_connect/api.py
|
||||||
|
homeassistant/components/aladdin_connect/application_credentials.py
|
||||||
|
homeassistant/components/aladdin_connect/cover.py
|
||||||
|
homeassistant/components/aladdin_connect/sensor.py
|
||||||
homeassistant/components/alarmdecoder/__init__.py
|
homeassistant/components/alarmdecoder/__init__.py
|
||||||
homeassistant/components/alarmdecoder/alarm_control_panel.py
|
homeassistant/components/alarmdecoder/alarm_control_panel.py
|
||||||
homeassistant/components/alarmdecoder/binary_sensor.py
|
homeassistant/components/alarmdecoder/binary_sensor.py
|
||||||
|
homeassistant/components/alarmdecoder/entity.py
|
||||||
homeassistant/components/alarmdecoder/sensor.py
|
homeassistant/components/alarmdecoder/sensor.py
|
||||||
homeassistant/components/alpha_vantage/sensor.py
|
homeassistant/components/alpha_vantage/sensor.py
|
||||||
homeassistant/components/amazon_polly/*
|
homeassistant/components/amazon_polly/*
|
||||||
homeassistant/components/ambiclimate/climate.py
|
|
||||||
homeassistant/components/ambient_station/__init__.py
|
homeassistant/components/ambient_station/__init__.py
|
||||||
homeassistant/components/ambient_station/binary_sensor.py
|
homeassistant/components/ambient_station/binary_sensor.py
|
||||||
homeassistant/components/ambient_station/entity.py
|
homeassistant/components/ambient_station/entity.py
|
||||||
@ -82,6 +87,12 @@ omit =
|
|||||||
homeassistant/components/aprilaire/climate.py
|
homeassistant/components/aprilaire/climate.py
|
||||||
homeassistant/components/aprilaire/coordinator.py
|
homeassistant/components/aprilaire/coordinator.py
|
||||||
homeassistant/components/aprilaire/entity.py
|
homeassistant/components/aprilaire/entity.py
|
||||||
|
homeassistant/components/aprilaire/sensor.py
|
||||||
|
homeassistant/components/apsystems/__init__.py
|
||||||
|
homeassistant/components/apsystems/coordinator.py
|
||||||
|
homeassistant/components/apsystems/entity.py
|
||||||
|
homeassistant/components/apsystems/number.py
|
||||||
|
homeassistant/components/apsystems/sensor.py
|
||||||
homeassistant/components/aqualogic/*
|
homeassistant/components/aqualogic/*
|
||||||
homeassistant/components/aquostv/media_player.py
|
homeassistant/components/aquostv/media_player.py
|
||||||
homeassistant/components/arcam_fmj/__init__.py
|
homeassistant/components/arcam_fmj/__init__.py
|
||||||
@ -111,6 +122,7 @@ omit =
|
|||||||
homeassistant/components/awair/coordinator.py
|
homeassistant/components/awair/coordinator.py
|
||||||
homeassistant/components/azure_service_bus/*
|
homeassistant/components/azure_service_bus/*
|
||||||
homeassistant/components/baf/__init__.py
|
homeassistant/components/baf/__init__.py
|
||||||
|
homeassistant/components/baf/binary_sensor.py
|
||||||
homeassistant/components/baf/climate.py
|
homeassistant/components/baf/climate.py
|
||||||
homeassistant/components/baf/entity.py
|
homeassistant/components/baf/entity.py
|
||||||
homeassistant/components/baf/fan.py
|
homeassistant/components/baf/fan.py
|
||||||
@ -119,8 +131,6 @@ omit =
|
|||||||
homeassistant/components/baf/sensor.py
|
homeassistant/components/baf/sensor.py
|
||||||
homeassistant/components/baf/switch.py
|
homeassistant/components/baf/switch.py
|
||||||
homeassistant/components/baidu/tts.py
|
homeassistant/components/baidu/tts.py
|
||||||
homeassistant/components/bang_olufsen/__init__.py
|
|
||||||
homeassistant/components/bang_olufsen/const.py
|
|
||||||
homeassistant/components/bang_olufsen/entity.py
|
homeassistant/components/bang_olufsen/entity.py
|
||||||
homeassistant/components/bang_olufsen/media_player.py
|
homeassistant/components/bang_olufsen/media_player.py
|
||||||
homeassistant/components/bang_olufsen/util.py
|
homeassistant/components/bang_olufsen/util.py
|
||||||
@ -141,12 +151,7 @@ omit =
|
|||||||
homeassistant/components/bloomsky/*
|
homeassistant/components/bloomsky/*
|
||||||
homeassistant/components/bluesound/*
|
homeassistant/components/bluesound/*
|
||||||
homeassistant/components/bluetooth_tracker/*
|
homeassistant/components/bluetooth_tracker/*
|
||||||
homeassistant/components/bmw_connected_drive/__init__.py
|
|
||||||
homeassistant/components/bmw_connected_drive/binary_sensor.py
|
|
||||||
homeassistant/components/bmw_connected_drive/coordinator.py
|
|
||||||
homeassistant/components/bmw_connected_drive/lock.py
|
|
||||||
homeassistant/components/bmw_connected_drive/notify.py
|
homeassistant/components/bmw_connected_drive/notify.py
|
||||||
homeassistant/components/bmw_connected_drive/sensor.py
|
|
||||||
homeassistant/components/bosch_shc/__init__.py
|
homeassistant/components/bosch_shc/__init__.py
|
||||||
homeassistant/components/bosch_shc/binary_sensor.py
|
homeassistant/components/bosch_shc/binary_sensor.py
|
||||||
homeassistant/components/bosch_shc/cover.py
|
homeassistant/components/bosch_shc/cover.py
|
||||||
@ -177,7 +182,6 @@ omit =
|
|||||||
homeassistant/components/canary/camera.py
|
homeassistant/components/canary/camera.py
|
||||||
homeassistant/components/cert_expiry/helper.py
|
homeassistant/components/cert_expiry/helper.py
|
||||||
homeassistant/components/channels/*
|
homeassistant/components/channels/*
|
||||||
homeassistant/components/circuit/*
|
|
||||||
homeassistant/components/cisco_ios/device_tracker.py
|
homeassistant/components/cisco_ios/device_tracker.py
|
||||||
homeassistant/components/cisco_mobility_express/device_tracker.py
|
homeassistant/components/cisco_mobility_express/device_tracker.py
|
||||||
homeassistant/components/cisco_webex_teams/notify.py
|
homeassistant/components/cisco_webex_teams/notify.py
|
||||||
@ -192,7 +196,6 @@ omit =
|
|||||||
homeassistant/components/comelit/__init__.py
|
homeassistant/components/comelit/__init__.py
|
||||||
homeassistant/components/comelit/alarm_control_panel.py
|
homeassistant/components/comelit/alarm_control_panel.py
|
||||||
homeassistant/components/comelit/climate.py
|
homeassistant/components/comelit/climate.py
|
||||||
homeassistant/components/comelit/const.py
|
|
||||||
homeassistant/components/comelit/coordinator.py
|
homeassistant/components/comelit/coordinator.py
|
||||||
homeassistant/components/comelit/cover.py
|
homeassistant/components/comelit/cover.py
|
||||||
homeassistant/components/comelit/humidifier.py
|
homeassistant/components/comelit/humidifier.py
|
||||||
@ -255,9 +258,6 @@ omit =
|
|||||||
homeassistant/components/dormakaba_dkey/sensor.py
|
homeassistant/components/dormakaba_dkey/sensor.py
|
||||||
homeassistant/components/dovado/*
|
homeassistant/components/dovado/*
|
||||||
homeassistant/components/downloader/__init__.py
|
homeassistant/components/downloader/__init__.py
|
||||||
homeassistant/components/dsmr_reader/__init__.py
|
|
||||||
homeassistant/components/dsmr_reader/definitions.py
|
|
||||||
homeassistant/components/dsmr_reader/sensor.py
|
|
||||||
homeassistant/components/dte_energy_bridge/sensor.py
|
homeassistant/components/dte_energy_bridge/sensor.py
|
||||||
homeassistant/components/dublin_bus_transport/sensor.py
|
homeassistant/components/dublin_bus_transport/sensor.py
|
||||||
homeassistant/components/dunehd/__init__.py
|
homeassistant/components/dunehd/__init__.py
|
||||||
@ -269,7 +269,6 @@ omit =
|
|||||||
homeassistant/components/duotecno/entity.py
|
homeassistant/components/duotecno/entity.py
|
||||||
homeassistant/components/duotecno/light.py
|
homeassistant/components/duotecno/light.py
|
||||||
homeassistant/components/duotecno/switch.py
|
homeassistant/components/duotecno/switch.py
|
||||||
homeassistant/components/dwd_weather_warnings/const.py
|
|
||||||
homeassistant/components/dwd_weather_warnings/coordinator.py
|
homeassistant/components/dwd_weather_warnings/coordinator.py
|
||||||
homeassistant/components/dwd_weather_warnings/sensor.py
|
homeassistant/components/dwd_weather_warnings/sensor.py
|
||||||
homeassistant/components/dweet/*
|
homeassistant/components/dweet/*
|
||||||
@ -326,8 +325,7 @@ omit =
|
|||||||
homeassistant/components/elmax/__init__.py
|
homeassistant/components/elmax/__init__.py
|
||||||
homeassistant/components/elmax/alarm_control_panel.py
|
homeassistant/components/elmax/alarm_control_panel.py
|
||||||
homeassistant/components/elmax/binary_sensor.py
|
homeassistant/components/elmax/binary_sensor.py
|
||||||
homeassistant/components/elmax/common.py
|
homeassistant/components/elmax/coordinator.py
|
||||||
homeassistant/components/elmax/const.py
|
|
||||||
homeassistant/components/elmax/cover.py
|
homeassistant/components/elmax/cover.py
|
||||||
homeassistant/components/elmax/switch.py
|
homeassistant/components/elmax/switch.py
|
||||||
homeassistant/components/elv/*
|
homeassistant/components/elv/*
|
||||||
@ -370,7 +368,6 @@ omit =
|
|||||||
homeassistant/components/epson/media_player.py
|
homeassistant/components/epson/media_player.py
|
||||||
homeassistant/components/eq3btsmart/__init__.py
|
homeassistant/components/eq3btsmart/__init__.py
|
||||||
homeassistant/components/eq3btsmart/climate.py
|
homeassistant/components/eq3btsmart/climate.py
|
||||||
homeassistant/components/eq3btsmart/const.py
|
|
||||||
homeassistant/components/eq3btsmart/entity.py
|
homeassistant/components/eq3btsmart/entity.py
|
||||||
homeassistant/components/eq3btsmart/models.py
|
homeassistant/components/eq3btsmart/models.py
|
||||||
homeassistant/components/escea/__init__.py
|
homeassistant/components/escea/__init__.py
|
||||||
@ -462,8 +459,8 @@ omit =
|
|||||||
homeassistant/components/freebox/camera.py
|
homeassistant/components/freebox/camera.py
|
||||||
homeassistant/components/freebox/home_base.py
|
homeassistant/components/freebox/home_base.py
|
||||||
homeassistant/components/freebox/switch.py
|
homeassistant/components/freebox/switch.py
|
||||||
homeassistant/components/fritz/common.py
|
homeassistant/components/fritz/coordinator.py
|
||||||
homeassistant/components/fritz/device_tracker.py
|
homeassistant/components/fritz/entity.py
|
||||||
homeassistant/components/fritz/services.py
|
homeassistant/components/fritz/services.py
|
||||||
homeassistant/components/fritz/switch.py
|
homeassistant/components/fritz/switch.py
|
||||||
homeassistant/components/fritzbox_callmonitor/__init__.py
|
homeassistant/components/fritzbox_callmonitor/__init__.py
|
||||||
@ -473,10 +470,6 @@ omit =
|
|||||||
homeassistant/components/frontier_silicon/browse_media.py
|
homeassistant/components/frontier_silicon/browse_media.py
|
||||||
homeassistant/components/frontier_silicon/media_player.py
|
homeassistant/components/frontier_silicon/media_player.py
|
||||||
homeassistant/components/futurenow/light.py
|
homeassistant/components/futurenow/light.py
|
||||||
homeassistant/components/fyta/__init__.py
|
|
||||||
homeassistant/components/fyta/coordinator.py
|
|
||||||
homeassistant/components/fyta/entity.py
|
|
||||||
homeassistant/components/fyta/sensor.py
|
|
||||||
homeassistant/components/garadget/cover.py
|
homeassistant/components/garadget/cover.py
|
||||||
homeassistant/components/garages_amsterdam/__init__.py
|
homeassistant/components/garages_amsterdam/__init__.py
|
||||||
homeassistant/components/garages_amsterdam/binary_sensor.py
|
homeassistant/components/garages_amsterdam/binary_sensor.py
|
||||||
@ -505,7 +498,6 @@ omit =
|
|||||||
homeassistant/components/gpsd/sensor.py
|
homeassistant/components/gpsd/sensor.py
|
||||||
homeassistant/components/greenwave/light.py
|
homeassistant/components/greenwave/light.py
|
||||||
homeassistant/components/growatt_server/__init__.py
|
homeassistant/components/growatt_server/__init__.py
|
||||||
homeassistant/components/growatt_server/const.py
|
|
||||||
homeassistant/components/growatt_server/sensor.py
|
homeassistant/components/growatt_server/sensor.py
|
||||||
homeassistant/components/growatt_server/sensor_types/*
|
homeassistant/components/growatt_server/sensor_types/*
|
||||||
homeassistant/components/gstreamer/media_player.py
|
homeassistant/components/gstreamer/media_player.py
|
||||||
@ -545,7 +537,6 @@ omit =
|
|||||||
homeassistant/components/hko/weather.py
|
homeassistant/components/hko/weather.py
|
||||||
homeassistant/components/hlk_sw16/__init__.py
|
homeassistant/components/hlk_sw16/__init__.py
|
||||||
homeassistant/components/hlk_sw16/switch.py
|
homeassistant/components/hlk_sw16/switch.py
|
||||||
homeassistant/components/home_connect/binary_sensor.py
|
|
||||||
homeassistant/components/home_connect/entity.py
|
homeassistant/components/home_connect/entity.py
|
||||||
homeassistant/components/home_connect/light.py
|
homeassistant/components/home_connect/light.py
|
||||||
homeassistant/components/home_connect/switch.py
|
homeassistant/components/home_connect/switch.py
|
||||||
@ -599,7 +590,9 @@ omit =
|
|||||||
homeassistant/components/ifttt/alarm_control_panel.py
|
homeassistant/components/ifttt/alarm_control_panel.py
|
||||||
homeassistant/components/iglo/light.py
|
homeassistant/components/iglo/light.py
|
||||||
homeassistant/components/ihc/*
|
homeassistant/components/ihc/*
|
||||||
homeassistant/components/incomfort/*
|
homeassistant/components/incomfort/__init__.py
|
||||||
|
homeassistant/components/incomfort/climate.py
|
||||||
|
homeassistant/components/incomfort/water_heater.py
|
||||||
homeassistant/components/insteon/binary_sensor.py
|
homeassistant/components/insteon/binary_sensor.py
|
||||||
homeassistant/components/insteon/climate.py
|
homeassistant/components/insteon/climate.py
|
||||||
homeassistant/components/insteon/cover.py
|
homeassistant/components/insteon/cover.py
|
||||||
@ -629,6 +622,7 @@ omit =
|
|||||||
homeassistant/components/irish_rail_transport/sensor.py
|
homeassistant/components/irish_rail_transport/sensor.py
|
||||||
homeassistant/components/iss/__init__.py
|
homeassistant/components/iss/__init__.py
|
||||||
homeassistant/components/iss/sensor.py
|
homeassistant/components/iss/sensor.py
|
||||||
|
homeassistant/components/ista_ecotrend/coordinator.py
|
||||||
homeassistant/components/isy994/__init__.py
|
homeassistant/components/isy994/__init__.py
|
||||||
homeassistant/components/isy994/binary_sensor.py
|
homeassistant/components/isy994/binary_sensor.py
|
||||||
homeassistant/components/isy994/button.py
|
homeassistant/components/isy994/button.py
|
||||||
@ -685,6 +679,7 @@ omit =
|
|||||||
homeassistant/components/konnected/panel.py
|
homeassistant/components/konnected/panel.py
|
||||||
homeassistant/components/konnected/switch.py
|
homeassistant/components/konnected/switch.py
|
||||||
homeassistant/components/kostal_plenticore/__init__.py
|
homeassistant/components/kostal_plenticore/__init__.py
|
||||||
|
homeassistant/components/kostal_plenticore/coordinator.py
|
||||||
homeassistant/components/kostal_plenticore/helper.py
|
homeassistant/components/kostal_plenticore/helper.py
|
||||||
homeassistant/components/kostal_plenticore/select.py
|
homeassistant/components/kostal_plenticore/select.py
|
||||||
homeassistant/components/kostal_plenticore/sensor.py
|
homeassistant/components/kostal_plenticore/sensor.py
|
||||||
@ -732,7 +727,6 @@ omit =
|
|||||||
homeassistant/components/lookin/sensor.py
|
homeassistant/components/lookin/sensor.py
|
||||||
homeassistant/components/loqed/sensor.py
|
homeassistant/components/loqed/sensor.py
|
||||||
homeassistant/components/luci/device_tracker.py
|
homeassistant/components/luci/device_tracker.py
|
||||||
homeassistant/components/luftdaten/sensor.py
|
|
||||||
homeassistant/components/lupusec/__init__.py
|
homeassistant/components/lupusec/__init__.py
|
||||||
homeassistant/components/lupusec/alarm_control_panel.py
|
homeassistant/components/lupusec/alarm_control_panel.py
|
||||||
homeassistant/components/lupusec/binary_sensor.py
|
homeassistant/components/lupusec/binary_sensor.py
|
||||||
@ -762,6 +756,7 @@ omit =
|
|||||||
homeassistant/components/matrix/__init__.py
|
homeassistant/components/matrix/__init__.py
|
||||||
homeassistant/components/matrix/notify.py
|
homeassistant/components/matrix/notify.py
|
||||||
homeassistant/components/matter/__init__.py
|
homeassistant/components/matter/__init__.py
|
||||||
|
homeassistant/components/matter/fan.py
|
||||||
homeassistant/components/meater/__init__.py
|
homeassistant/components/meater/__init__.py
|
||||||
homeassistant/components/meater/sensor.py
|
homeassistant/components/meater/sensor.py
|
||||||
homeassistant/components/medcom_ble/__init__.py
|
homeassistant/components/medcom_ble/__init__.py
|
||||||
@ -788,7 +783,7 @@ omit =
|
|||||||
homeassistant/components/microbees/application_credentials.py
|
homeassistant/components/microbees/application_credentials.py
|
||||||
homeassistant/components/microbees/binary_sensor.py
|
homeassistant/components/microbees/binary_sensor.py
|
||||||
homeassistant/components/microbees/button.py
|
homeassistant/components/microbees/button.py
|
||||||
homeassistant/components/microbees/const.py
|
homeassistant/components/microbees/climate.py
|
||||||
homeassistant/components/microbees/coordinator.py
|
homeassistant/components/microbees/coordinator.py
|
||||||
homeassistant/components/microbees/cover.py
|
homeassistant/components/microbees/cover.py
|
||||||
homeassistant/components/microbees/entity.py
|
homeassistant/components/microbees/entity.py
|
||||||
@ -796,7 +791,7 @@ omit =
|
|||||||
homeassistant/components/microbees/sensor.py
|
homeassistant/components/microbees/sensor.py
|
||||||
homeassistant/components/microbees/switch.py
|
homeassistant/components/microbees/switch.py
|
||||||
homeassistant/components/microsoft/tts.py
|
homeassistant/components/microsoft/tts.py
|
||||||
homeassistant/components/mikrotik/hub.py
|
homeassistant/components/mikrotik/coordinator.py
|
||||||
homeassistant/components/mill/climate.py
|
homeassistant/components/mill/climate.py
|
||||||
homeassistant/components/mill/sensor.py
|
homeassistant/components/mill/sensor.py
|
||||||
homeassistant/components/minio/minio_helper.py
|
homeassistant/components/minio/minio_helper.py
|
||||||
@ -807,10 +802,10 @@ omit =
|
|||||||
homeassistant/components/mochad/switch.py
|
homeassistant/components/mochad/switch.py
|
||||||
homeassistant/components/modem_callerid/button.py
|
homeassistant/components/modem_callerid/button.py
|
||||||
homeassistant/components/modem_callerid/sensor.py
|
homeassistant/components/modem_callerid/sensor.py
|
||||||
homeassistant/components/moehlenhoff_alpha2/__init__.py
|
|
||||||
homeassistant/components/moehlenhoff_alpha2/binary_sensor.py
|
|
||||||
homeassistant/components/moehlenhoff_alpha2/climate.py
|
homeassistant/components/moehlenhoff_alpha2/climate.py
|
||||||
homeassistant/components/moehlenhoff_alpha2/sensor.py
|
homeassistant/components/moehlenhoff_alpha2/coordinator.py
|
||||||
|
homeassistant/components/monzo/__init__.py
|
||||||
|
homeassistant/components/monzo/api.py
|
||||||
homeassistant/components/motion_blinds/__init__.py
|
homeassistant/components/motion_blinds/__init__.py
|
||||||
homeassistant/components/motion_blinds/coordinator.py
|
homeassistant/components/motion_blinds/coordinator.py
|
||||||
homeassistant/components/motion_blinds/cover.py
|
homeassistant/components/motion_blinds/cover.py
|
||||||
@ -821,6 +816,7 @@ omit =
|
|||||||
homeassistant/components/motionblinds_ble/cover.py
|
homeassistant/components/motionblinds_ble/cover.py
|
||||||
homeassistant/components/motionblinds_ble/entity.py
|
homeassistant/components/motionblinds_ble/entity.py
|
||||||
homeassistant/components/motionblinds_ble/select.py
|
homeassistant/components/motionblinds_ble/select.py
|
||||||
|
homeassistant/components/motionblinds_ble/sensor.py
|
||||||
homeassistant/components/motionmount/__init__.py
|
homeassistant/components/motionmount/__init__.py
|
||||||
homeassistant/components/motionmount/binary_sensor.py
|
homeassistant/components/motionmount/binary_sensor.py
|
||||||
homeassistant/components/motionmount/entity.py
|
homeassistant/components/motionmount/entity.py
|
||||||
@ -858,7 +854,9 @@ omit =
|
|||||||
homeassistant/components/nad/media_player.py
|
homeassistant/components/nad/media_player.py
|
||||||
homeassistant/components/nanoleaf/__init__.py
|
homeassistant/components/nanoleaf/__init__.py
|
||||||
homeassistant/components/nanoleaf/button.py
|
homeassistant/components/nanoleaf/button.py
|
||||||
|
homeassistant/components/nanoleaf/coordinator.py
|
||||||
homeassistant/components/nanoleaf/entity.py
|
homeassistant/components/nanoleaf/entity.py
|
||||||
|
homeassistant/components/nanoleaf/event.py
|
||||||
homeassistant/components/nanoleaf/light.py
|
homeassistant/components/nanoleaf/light.py
|
||||||
homeassistant/components/neato/__init__.py
|
homeassistant/components/neato/__init__.py
|
||||||
homeassistant/components/neato/api.py
|
homeassistant/components/neato/api.py
|
||||||
@ -920,9 +918,8 @@ omit =
|
|||||||
homeassistant/components/notion/util.py
|
homeassistant/components/notion/util.py
|
||||||
homeassistant/components/nsw_fuel_station/sensor.py
|
homeassistant/components/nsw_fuel_station/sensor.py
|
||||||
homeassistant/components/nuki/__init__.py
|
homeassistant/components/nuki/__init__.py
|
||||||
homeassistant/components/nuki/binary_sensor.py
|
homeassistant/components/nuki/coordinator.py
|
||||||
homeassistant/components/nuki/lock.py
|
homeassistant/components/nuki/lock.py
|
||||||
homeassistant/components/nuki/sensor.py
|
|
||||||
homeassistant/components/nx584/alarm_control_panel.py
|
homeassistant/components/nx584/alarm_control_panel.py
|
||||||
homeassistant/components/oasa_telematics/sensor.py
|
homeassistant/components/oasa_telematics/sensor.py
|
||||||
homeassistant/components/obihai/__init__.py
|
homeassistant/components/obihai/__init__.py
|
||||||
@ -935,7 +932,7 @@ omit =
|
|||||||
homeassistant/components/ohmconnect/sensor.py
|
homeassistant/components/ohmconnect/sensor.py
|
||||||
homeassistant/components/ombi/*
|
homeassistant/components/ombi/*
|
||||||
homeassistant/components/omnilogic/__init__.py
|
homeassistant/components/omnilogic/__init__.py
|
||||||
homeassistant/components/omnilogic/common.py
|
homeassistant/components/omnilogic/coordinator.py
|
||||||
homeassistant/components/omnilogic/sensor.py
|
homeassistant/components/omnilogic/sensor.py
|
||||||
homeassistant/components/omnilogic/switch.py
|
homeassistant/components/omnilogic/switch.py
|
||||||
homeassistant/components/ondilo_ico/__init__.py
|
homeassistant/components/ondilo_ico/__init__.py
|
||||||
@ -963,7 +960,6 @@ omit =
|
|||||||
homeassistant/components/opengarage/sensor.py
|
homeassistant/components/opengarage/sensor.py
|
||||||
homeassistant/components/openhardwaremonitor/sensor.py
|
homeassistant/components/openhardwaremonitor/sensor.py
|
||||||
homeassistant/components/openhome/__init__.py
|
homeassistant/components/openhome/__init__.py
|
||||||
homeassistant/components/openhome/const.py
|
|
||||||
homeassistant/components/openhome/media_player.py
|
homeassistant/components/openhome/media_player.py
|
||||||
homeassistant/components/opensensemap/air_quality.py
|
homeassistant/components/opensensemap/air_quality.py
|
||||||
homeassistant/components/opentherm_gw/__init__.py
|
homeassistant/components/opentherm_gw/__init__.py
|
||||||
@ -975,9 +971,10 @@ omit =
|
|||||||
homeassistant/components/openuv/coordinator.py
|
homeassistant/components/openuv/coordinator.py
|
||||||
homeassistant/components/openuv/sensor.py
|
homeassistant/components/openuv/sensor.py
|
||||||
homeassistant/components/openweathermap/__init__.py
|
homeassistant/components/openweathermap/__init__.py
|
||||||
|
homeassistant/components/openweathermap/coordinator.py
|
||||||
|
homeassistant/components/openweathermap/repairs.py
|
||||||
homeassistant/components/openweathermap/sensor.py
|
homeassistant/components/openweathermap/sensor.py
|
||||||
homeassistant/components/openweathermap/weather.py
|
homeassistant/components/openweathermap/weather.py
|
||||||
homeassistant/components/openweathermap/weather_update_coordinator.py
|
|
||||||
homeassistant/components/opnsense/__init__.py
|
homeassistant/components/opnsense/__init__.py
|
||||||
homeassistant/components/opnsense/device_tracker.py
|
homeassistant/components/opnsense/device_tracker.py
|
||||||
homeassistant/components/opower/__init__.py
|
homeassistant/components/opower/__init__.py
|
||||||
@ -987,7 +984,8 @@ omit =
|
|||||||
homeassistant/components/oru/*
|
homeassistant/components/oru/*
|
||||||
homeassistant/components/orvibo/switch.py
|
homeassistant/components/orvibo/switch.py
|
||||||
homeassistant/components/osoenergy/__init__.py
|
homeassistant/components/osoenergy/__init__.py
|
||||||
homeassistant/components/osoenergy/const.py
|
homeassistant/components/osoenergy/binary_sensor.py
|
||||||
|
homeassistant/components/osoenergy/entity.py
|
||||||
homeassistant/components/osoenergy/sensor.py
|
homeassistant/components/osoenergy/sensor.py
|
||||||
homeassistant/components/osoenergy/water_heater.py
|
homeassistant/components/osoenergy/water_heater.py
|
||||||
homeassistant/components/osramlightify/light.py
|
homeassistant/components/osramlightify/light.py
|
||||||
@ -1024,6 +1022,7 @@ omit =
|
|||||||
homeassistant/components/permobil/entity.py
|
homeassistant/components/permobil/entity.py
|
||||||
homeassistant/components/permobil/sensor.py
|
homeassistant/components/permobil/sensor.py
|
||||||
homeassistant/components/philips_js/__init__.py
|
homeassistant/components/philips_js/__init__.py
|
||||||
|
homeassistant/components/philips_js/coordinator.py
|
||||||
homeassistant/components/philips_js/light.py
|
homeassistant/components/philips_js/light.py
|
||||||
homeassistant/components/philips_js/media_player.py
|
homeassistant/components/philips_js/media_player.py
|
||||||
homeassistant/components/philips_js/remote.py
|
homeassistant/components/philips_js/remote.py
|
||||||
@ -1032,7 +1031,6 @@ omit =
|
|||||||
homeassistant/components/picotts/tts.py
|
homeassistant/components/picotts/tts.py
|
||||||
homeassistant/components/pilight/base_class.py
|
homeassistant/components/pilight/base_class.py
|
||||||
homeassistant/components/pilight/binary_sensor.py
|
homeassistant/components/pilight/binary_sensor.py
|
||||||
homeassistant/components/pilight/const.py
|
|
||||||
homeassistant/components/pilight/light.py
|
homeassistant/components/pilight/light.py
|
||||||
homeassistant/components/pilight/switch.py
|
homeassistant/components/pilight/switch.py
|
||||||
homeassistant/components/ping/__init__.py
|
homeassistant/components/ping/__init__.py
|
||||||
@ -1051,11 +1049,6 @@ omit =
|
|||||||
homeassistant/components/point/alarm_control_panel.py
|
homeassistant/components/point/alarm_control_panel.py
|
||||||
homeassistant/components/point/binary_sensor.py
|
homeassistant/components/point/binary_sensor.py
|
||||||
homeassistant/components/point/sensor.py
|
homeassistant/components/point/sensor.py
|
||||||
homeassistant/components/poolsense/__init__.py
|
|
||||||
homeassistant/components/poolsense/binary_sensor.py
|
|
||||||
homeassistant/components/poolsense/coordinator.py
|
|
||||||
homeassistant/components/poolsense/entity.py
|
|
||||||
homeassistant/components/poolsense/sensor.py
|
|
||||||
homeassistant/components/powerwall/__init__.py
|
homeassistant/components/powerwall/__init__.py
|
||||||
homeassistant/components/progettihwsw/__init__.py
|
homeassistant/components/progettihwsw/__init__.py
|
||||||
homeassistant/components/progettihwsw/binary_sensor.py
|
homeassistant/components/progettihwsw/binary_sensor.py
|
||||||
@ -1071,7 +1064,6 @@ omit =
|
|||||||
homeassistant/components/pushbullet/sensor.py
|
homeassistant/components/pushbullet/sensor.py
|
||||||
homeassistant/components/pushover/notify.py
|
homeassistant/components/pushover/notify.py
|
||||||
homeassistant/components/pushsafer/notify.py
|
homeassistant/components/pushsafer/notify.py
|
||||||
homeassistant/components/pyload/sensor.py
|
|
||||||
homeassistant/components/qbittorrent/__init__.py
|
homeassistant/components/qbittorrent/__init__.py
|
||||||
homeassistant/components/qbittorrent/coordinator.py
|
homeassistant/components/qbittorrent/coordinator.py
|
||||||
homeassistant/components/qbittorrent/sensor.py
|
homeassistant/components/qbittorrent/sensor.py
|
||||||
@ -1082,7 +1074,6 @@ omit =
|
|||||||
homeassistant/components/quantum_gateway/device_tracker.py
|
homeassistant/components/quantum_gateway/device_tracker.py
|
||||||
homeassistant/components/qvr_pro/*
|
homeassistant/components/qvr_pro/*
|
||||||
homeassistant/components/rabbitair/__init__.py
|
homeassistant/components/rabbitair/__init__.py
|
||||||
homeassistant/components/rabbitair/const.py
|
|
||||||
homeassistant/components/rabbitair/coordinator.py
|
homeassistant/components/rabbitair/coordinator.py
|
||||||
homeassistant/components/rabbitair/entity.py
|
homeassistant/components/rabbitair/entity.py
|
||||||
homeassistant/components/rabbitair/fan.py
|
homeassistant/components/rabbitair/fan.py
|
||||||
@ -1105,6 +1096,7 @@ omit =
|
|||||||
homeassistant/components/rainmachine/__init__.py
|
homeassistant/components/rainmachine/__init__.py
|
||||||
homeassistant/components/rainmachine/binary_sensor.py
|
homeassistant/components/rainmachine/binary_sensor.py
|
||||||
homeassistant/components/rainmachine/button.py
|
homeassistant/components/rainmachine/button.py
|
||||||
|
homeassistant/components/rainmachine/coordinator.py
|
||||||
homeassistant/components/rainmachine/select.py
|
homeassistant/components/rainmachine/select.py
|
||||||
homeassistant/components/rainmachine/sensor.py
|
homeassistant/components/rainmachine/sensor.py
|
||||||
homeassistant/components/rainmachine/switch.py
|
homeassistant/components/rainmachine/switch.py
|
||||||
@ -1119,6 +1111,7 @@ omit =
|
|||||||
homeassistant/components/refoss/bridge.py
|
homeassistant/components/refoss/bridge.py
|
||||||
homeassistant/components/refoss/coordinator.py
|
homeassistant/components/refoss/coordinator.py
|
||||||
homeassistant/components/refoss/entity.py
|
homeassistant/components/refoss/entity.py
|
||||||
|
homeassistant/components/refoss/sensor.py
|
||||||
homeassistant/components/refoss/switch.py
|
homeassistant/components/refoss/switch.py
|
||||||
homeassistant/components/refoss/util.py
|
homeassistant/components/refoss/util.py
|
||||||
homeassistant/components/rejseplanen/sensor.py
|
homeassistant/components/rejseplanen/sensor.py
|
||||||
@ -1127,7 +1120,6 @@ omit =
|
|||||||
homeassistant/components/renson/__init__.py
|
homeassistant/components/renson/__init__.py
|
||||||
homeassistant/components/renson/binary_sensor.py
|
homeassistant/components/renson/binary_sensor.py
|
||||||
homeassistant/components/renson/button.py
|
homeassistant/components/renson/button.py
|
||||||
homeassistant/components/renson/const.py
|
|
||||||
homeassistant/components/renson/coordinator.py
|
homeassistant/components/renson/coordinator.py
|
||||||
homeassistant/components/renson/entity.py
|
homeassistant/components/renson/entity.py
|
||||||
homeassistant/components/renson/fan.py
|
homeassistant/components/renson/fan.py
|
||||||
@ -1196,13 +1188,11 @@ omit =
|
|||||||
homeassistant/components/schluter/*
|
homeassistant/components/schluter/*
|
||||||
homeassistant/components/screenlogic/binary_sensor.py
|
homeassistant/components/screenlogic/binary_sensor.py
|
||||||
homeassistant/components/screenlogic/climate.py
|
homeassistant/components/screenlogic/climate.py
|
||||||
homeassistant/components/screenlogic/const.py
|
|
||||||
homeassistant/components/screenlogic/coordinator.py
|
homeassistant/components/screenlogic/coordinator.py
|
||||||
homeassistant/components/screenlogic/entity.py
|
homeassistant/components/screenlogic/entity.py
|
||||||
homeassistant/components/screenlogic/light.py
|
homeassistant/components/screenlogic/light.py
|
||||||
homeassistant/components/screenlogic/number.py
|
homeassistant/components/screenlogic/number.py
|
||||||
homeassistant/components/screenlogic/sensor.py
|
homeassistant/components/screenlogic/sensor.py
|
||||||
homeassistant/components/screenlogic/services.py
|
|
||||||
homeassistant/components/screenlogic/switch.py
|
homeassistant/components/screenlogic/switch.py
|
||||||
homeassistant/components/scsgate/*
|
homeassistant/components/scsgate/*
|
||||||
homeassistant/components/sendgrid/notify.py
|
homeassistant/components/sendgrid/notify.py
|
||||||
@ -1255,7 +1245,6 @@ omit =
|
|||||||
homeassistant/components/smappee/switch.py
|
homeassistant/components/smappee/switch.py
|
||||||
homeassistant/components/smarty/*
|
homeassistant/components/smarty/*
|
||||||
homeassistant/components/sms/__init__.py
|
homeassistant/components/sms/__init__.py
|
||||||
homeassistant/components/sms/const.py
|
|
||||||
homeassistant/components/sms/coordinator.py
|
homeassistant/components/sms/coordinator.py
|
||||||
homeassistant/components/sms/gateway.py
|
homeassistant/components/sms/gateway.py
|
||||||
homeassistant/components/sms/notify.py
|
homeassistant/components/sms/notify.py
|
||||||
@ -1271,9 +1260,6 @@ omit =
|
|||||||
homeassistant/components/solaredge/__init__.py
|
homeassistant/components/solaredge/__init__.py
|
||||||
homeassistant/components/solaredge/coordinator.py
|
homeassistant/components/solaredge/coordinator.py
|
||||||
homeassistant/components/solaredge_local/sensor.py
|
homeassistant/components/solaredge_local/sensor.py
|
||||||
homeassistant/components/solarlog/__init__.py
|
|
||||||
homeassistant/components/solarlog/coordinator.py
|
|
||||||
homeassistant/components/solarlog/sensor.py
|
|
||||||
homeassistant/components/solax/__init__.py
|
homeassistant/components/solax/__init__.py
|
||||||
homeassistant/components/solax/sensor.py
|
homeassistant/components/solax/sensor.py
|
||||||
homeassistant/components/soma/__init__.py
|
homeassistant/components/soma/__init__.py
|
||||||
@ -1349,6 +1335,7 @@ omit =
|
|||||||
homeassistant/components/supla/*
|
homeassistant/components/supla/*
|
||||||
homeassistant/components/surepetcare/__init__.py
|
homeassistant/components/surepetcare/__init__.py
|
||||||
homeassistant/components/surepetcare/binary_sensor.py
|
homeassistant/components/surepetcare/binary_sensor.py
|
||||||
|
homeassistant/components/surepetcare/coordinator.py
|
||||||
homeassistant/components/surepetcare/entity.py
|
homeassistant/components/surepetcare/entity.py
|
||||||
homeassistant/components/surepetcare/sensor.py
|
homeassistant/components/surepetcare/sensor.py
|
||||||
homeassistant/components/swiss_hydrological_data/sensor.py
|
homeassistant/components/swiss_hydrological_data/sensor.py
|
||||||
@ -1377,6 +1364,7 @@ omit =
|
|||||||
homeassistant/components/switchbot_cloud/climate.py
|
homeassistant/components/switchbot_cloud/climate.py
|
||||||
homeassistant/components/switchbot_cloud/coordinator.py
|
homeassistant/components/switchbot_cloud/coordinator.py
|
||||||
homeassistant/components/switchbot_cloud/entity.py
|
homeassistant/components/switchbot_cloud/entity.py
|
||||||
|
homeassistant/components/switchbot_cloud/sensor.py
|
||||||
homeassistant/components/switchbot_cloud/switch.py
|
homeassistant/components/switchbot_cloud/switch.py
|
||||||
homeassistant/components/switchmate/switch.py
|
homeassistant/components/switchmate/switch.py
|
||||||
homeassistant/components/syncthing/__init__.py
|
homeassistant/components/syncthing/__init__.py
|
||||||
@ -1435,12 +1423,11 @@ omit =
|
|||||||
homeassistant/components/tensorflow/image_processing.py
|
homeassistant/components/tensorflow/image_processing.py
|
||||||
homeassistant/components/tfiac/climate.py
|
homeassistant/components/tfiac/climate.py
|
||||||
homeassistant/components/thermoworks_smoke/sensor.py
|
homeassistant/components/thermoworks_smoke/sensor.py
|
||||||
homeassistant/components/thethingsnetwork/*
|
|
||||||
homeassistant/components/thingspeak/*
|
homeassistant/components/thingspeak/*
|
||||||
homeassistant/components/thinkingcleaner/*
|
homeassistant/components/thinkingcleaner/*
|
||||||
homeassistant/components/thomson/device_tracker.py
|
homeassistant/components/thomson/device_tracker.py
|
||||||
homeassistant/components/tibber/__init__.py
|
homeassistant/components/tibber/__init__.py
|
||||||
homeassistant/components/tibber/notify.py
|
homeassistant/components/tibber/coordinator.py
|
||||||
homeassistant/components/tibber/sensor.py
|
homeassistant/components/tibber/sensor.py
|
||||||
homeassistant/components/tikteck/light.py
|
homeassistant/components/tikteck/light.py
|
||||||
homeassistant/components/tile/__init__.py
|
homeassistant/components/tile/__init__.py
|
||||||
@ -1482,12 +1469,6 @@ omit =
|
|||||||
homeassistant/components/traccar_server/entity.py
|
homeassistant/components/traccar_server/entity.py
|
||||||
homeassistant/components/traccar_server/helpers.py
|
homeassistant/components/traccar_server/helpers.py
|
||||||
homeassistant/components/traccar_server/sensor.py
|
homeassistant/components/traccar_server/sensor.py
|
||||||
homeassistant/components/tractive/__init__.py
|
|
||||||
homeassistant/components/tractive/binary_sensor.py
|
|
||||||
homeassistant/components/tractive/device_tracker.py
|
|
||||||
homeassistant/components/tractive/entity.py
|
|
||||||
homeassistant/components/tractive/sensor.py
|
|
||||||
homeassistant/components/tractive/switch.py
|
|
||||||
homeassistant/components/tradfri/__init__.py
|
homeassistant/components/tradfri/__init__.py
|
||||||
homeassistant/components/tradfri/base_class.py
|
homeassistant/components/tradfri/base_class.py
|
||||||
homeassistant/components/tradfri/coordinator.py
|
homeassistant/components/tradfri/coordinator.py
|
||||||
@ -1546,8 +1527,9 @@ omit =
|
|||||||
homeassistant/components/v2c/coordinator.py
|
homeassistant/components/v2c/coordinator.py
|
||||||
homeassistant/components/v2c/entity.py
|
homeassistant/components/v2c/entity.py
|
||||||
homeassistant/components/v2c/number.py
|
homeassistant/components/v2c/number.py
|
||||||
homeassistant/components/v2c/sensor.py
|
|
||||||
homeassistant/components/v2c/switch.py
|
homeassistant/components/v2c/switch.py
|
||||||
|
homeassistant/components/vallox/__init__.py
|
||||||
|
homeassistant/components/vallox/coordinator.py
|
||||||
homeassistant/components/vasttrafik/sensor.py
|
homeassistant/components/vasttrafik/sensor.py
|
||||||
homeassistant/components/velbus/__init__.py
|
homeassistant/components/velbus/__init__.py
|
||||||
homeassistant/components/velbus/binary_sensor.py
|
homeassistant/components/velbus/binary_sensor.py
|
||||||
@ -1562,9 +1544,8 @@ omit =
|
|||||||
homeassistant/components/velux/__init__.py
|
homeassistant/components/velux/__init__.py
|
||||||
homeassistant/components/velux/cover.py
|
homeassistant/components/velux/cover.py
|
||||||
homeassistant/components/velux/light.py
|
homeassistant/components/velux/light.py
|
||||||
homeassistant/components/venstar/__init__.py
|
|
||||||
homeassistant/components/venstar/binary_sensor.py
|
|
||||||
homeassistant/components/venstar/climate.py
|
homeassistant/components/venstar/climate.py
|
||||||
|
homeassistant/components/venstar/coordinator.py
|
||||||
homeassistant/components/venstar/sensor.py
|
homeassistant/components/venstar/sensor.py
|
||||||
homeassistant/components/verisure/__init__.py
|
homeassistant/components/verisure/__init__.py
|
||||||
homeassistant/components/verisure/alarm_control_panel.py
|
homeassistant/components/verisure/alarm_control_panel.py
|
||||||
@ -1575,6 +1556,7 @@ omit =
|
|||||||
homeassistant/components/verisure/sensor.py
|
homeassistant/components/verisure/sensor.py
|
||||||
homeassistant/components/verisure/switch.py
|
homeassistant/components/verisure/switch.py
|
||||||
homeassistant/components/versasense/*
|
homeassistant/components/versasense/*
|
||||||
|
homeassistant/components/vesync/__init__.py
|
||||||
homeassistant/components/vesync/fan.py
|
homeassistant/components/vesync/fan.py
|
||||||
homeassistant/components/vesync/light.py
|
homeassistant/components/vesync/light.py
|
||||||
homeassistant/components/vesync/sensor.py
|
homeassistant/components/vesync/sensor.py
|
||||||
@ -1597,7 +1579,6 @@ omit =
|
|||||||
homeassistant/components/vlc_telnet/media_player.py
|
homeassistant/components/vlc_telnet/media_player.py
|
||||||
homeassistant/components/vodafone_station/__init__.py
|
homeassistant/components/vodafone_station/__init__.py
|
||||||
homeassistant/components/vodafone_station/button.py
|
homeassistant/components/vodafone_station/button.py
|
||||||
homeassistant/components/vodafone_station/const.py
|
|
||||||
homeassistant/components/vodafone_station/coordinator.py
|
homeassistant/components/vodafone_station/coordinator.py
|
||||||
homeassistant/components/vodafone_station/device_tracker.py
|
homeassistant/components/vodafone_station/device_tracker.py
|
||||||
homeassistant/components/vodafone_station/sensor.py
|
homeassistant/components/vodafone_station/sensor.py
|
||||||
@ -1622,10 +1603,8 @@ omit =
|
|||||||
homeassistant/components/watttime/__init__.py
|
homeassistant/components/watttime/__init__.py
|
||||||
homeassistant/components/watttime/sensor.py
|
homeassistant/components/watttime/sensor.py
|
||||||
homeassistant/components/weatherflow/__init__.py
|
homeassistant/components/weatherflow/__init__.py
|
||||||
homeassistant/components/weatherflow/const.py
|
|
||||||
homeassistant/components/weatherflow/sensor.py
|
homeassistant/components/weatherflow/sensor.py
|
||||||
homeassistant/components/weatherflow_cloud/__init__.py
|
homeassistant/components/weatherflow_cloud/__init__.py
|
||||||
homeassistant/components/weatherflow_cloud/const.py
|
|
||||||
homeassistant/components/weatherflow_cloud/coordinator.py
|
homeassistant/components/weatherflow_cloud/coordinator.py
|
||||||
homeassistant/components/weatherflow_cloud/weather.py
|
homeassistant/components/weatherflow_cloud/weather.py
|
||||||
homeassistant/components/wiffi/__init__.py
|
homeassistant/components/wiffi/__init__.py
|
||||||
@ -1643,6 +1622,7 @@ omit =
|
|||||||
homeassistant/components/xbox/base_sensor.py
|
homeassistant/components/xbox/base_sensor.py
|
||||||
homeassistant/components/xbox/binary_sensor.py
|
homeassistant/components/xbox/binary_sensor.py
|
||||||
homeassistant/components/xbox/browse_media.py
|
homeassistant/components/xbox/browse_media.py
|
||||||
|
homeassistant/components/xbox/coordinator.py
|
||||||
homeassistant/components/xbox/media_player.py
|
homeassistant/components/xbox/media_player.py
|
||||||
homeassistant/components/xbox/remote.py
|
homeassistant/components/xbox/remote.py
|
||||||
homeassistant/components/xbox/sensor.py
|
homeassistant/components/xbox/sensor.py
|
||||||
@ -1675,10 +1655,7 @@ omit =
|
|||||||
homeassistant/components/xs1/*
|
homeassistant/components/xs1/*
|
||||||
homeassistant/components/yale_smart_alarm/__init__.py
|
homeassistant/components/yale_smart_alarm/__init__.py
|
||||||
homeassistant/components/yale_smart_alarm/alarm_control_panel.py
|
homeassistant/components/yale_smart_alarm/alarm_control_panel.py
|
||||||
homeassistant/components/yale_smart_alarm/binary_sensor.py
|
|
||||||
homeassistant/components/yale_smart_alarm/button.py
|
|
||||||
homeassistant/components/yale_smart_alarm/entity.py
|
homeassistant/components/yale_smart_alarm/entity.py
|
||||||
homeassistant/components/yale_smart_alarm/lock.py
|
|
||||||
homeassistant/components/yalexs_ble/__init__.py
|
homeassistant/components/yalexs_ble/__init__.py
|
||||||
homeassistant/components/yalexs_ble/binary_sensor.py
|
homeassistant/components/yalexs_ble/binary_sensor.py
|
||||||
homeassistant/components/yalexs_ble/entity.py
|
homeassistant/components/yalexs_ble/entity.py
|
||||||
@ -1719,10 +1696,6 @@ omit =
|
|||||||
homeassistant/components/zeroconf/models.py
|
homeassistant/components/zeroconf/models.py
|
||||||
homeassistant/components/zeroconf/usage.py
|
homeassistant/components/zeroconf/usage.py
|
||||||
homeassistant/components/zestimate/sensor.py
|
homeassistant/components/zestimate/sensor.py
|
||||||
homeassistant/components/zeversolar/__init__.py
|
|
||||||
homeassistant/components/zeversolar/coordinator.py
|
|
||||||
homeassistant/components/zeversolar/entity.py
|
|
||||||
homeassistant/components/zeversolar/sensor.py
|
|
||||||
homeassistant/components/zha/core/cluster_handlers/*
|
homeassistant/components/zha/core/cluster_handlers/*
|
||||||
homeassistant/components/zha/core/device.py
|
homeassistant/components/zha/core/device.py
|
||||||
homeassistant/components/zha/core/gateway.py
|
homeassistant/components/zha/core/gateway.py
|
||||||
|
@ -5,9 +5,11 @@
|
|||||||
"postCreateCommand": "script/setup",
|
"postCreateCommand": "script/setup",
|
||||||
"postStartCommand": "script/bootstrap",
|
"postStartCommand": "script/bootstrap",
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
"DEVCONTAINER": "1",
|
|
||||||
"PYTHONASYNCIODEBUG": "1"
|
"PYTHONASYNCIODEBUG": "1"
|
||||||
},
|
},
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||||
|
},
|
||||||
// Port 5683 udp is used by Shelly integration
|
// Port 5683 udp is used by Shelly integration
|
||||||
"appPort": ["8123:8123", "5683:5683/udp"],
|
"appPort": ["8123:8123", "5683:5683/udp"],
|
||||||
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
|
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
|
||||||
@ -20,13 +22,17 @@
|
|||||||
"visualstudioexptteam.vscodeintellicode",
|
"visualstudioexptteam.vscodeintellicode",
|
||||||
"redhat.vscode-yaml",
|
"redhat.vscode-yaml",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"GitHub.vscode-pull-request-github"
|
"GitHub.vscode-pull-request-github",
|
||||||
|
"GitHub.copilot"
|
||||||
],
|
],
|
||||||
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
|
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
|
||||||
"settings": {
|
"settings": {
|
||||||
"python.experiments.optOutFrom": ["pythonTestAdapter"],
|
"python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||||
"python.pythonPath": "/usr/local/bin/python",
|
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
|
||||||
|
"python.pythonPath": "/home/vscode/.local/ha-venv/bin/python",
|
||||||
|
"python.terminal.activateEnvInCurrentTerminal": true,
|
||||||
"python.testing.pytestArgs": ["--no-cov"],
|
"python.testing.pytestArgs": ["--no-cov"],
|
||||||
|
"pylint.importStrategy": "fromEnvironment",
|
||||||
"editor.formatOnPaste": false,
|
"editor.formatOnPaste": false,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnType": true,
|
"editor.formatOnType": true,
|
||||||
|
24
.github/workflows/builder.yml
vendored
24
.github/workflows/builder.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
|||||||
publish: ${{ steps.version.outputs.publish }}
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@ -90,11 +90,11 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: dawidd6/action-download-artifact@v3.1.4
|
uses: dawidd6/action-download-artifact@v6
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
repo: home-assistant/frontend
|
repo: home-assistant/frontend
|
||||||
@ -105,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download nightly wheels of intents
|
- name: Download nightly wheels of intents
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: dawidd6/action-download-artifact@v3.1.4
|
uses: dawidd6/action-download-artifact@v6
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
repo: home-assistant/intents-package
|
repo: home-assistant/intents-package
|
||||||
@ -190,7 +190,7 @@ jobs:
|
|||||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.1.0
|
uses: docker/login-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@ -242,7 +242,7 @@ jobs:
|
|||||||
- green
|
- green
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
@ -256,7 +256,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.1.0
|
uses: docker/login-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@ -279,7 +279,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
uses: home-assistant/actions/helpers/git-init@master
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
@ -320,7 +320,7 @@ jobs:
|
|||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.5.0
|
uses: sigstore/cosign-installer@v3.5.0
|
||||||
@ -329,14 +329,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
uses: docker/login-action@v3.1.0
|
uses: docker/login-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||||
uses: docker/login-action@v3.1.0
|
uses: docker/login-action@v3.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@ -450,7 +450,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
|
97
.github/workflows/ci.yaml
vendored
97
.github/workflows/ci.yaml
vendored
@ -33,10 +33,10 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CACHE_VERSION: 8
|
CACHE_VERSION: 9
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 8
|
MYPY_CACHE_VERSION: 8
|
||||||
HA_SHORT_VERSION: "2024.6"
|
HA_SHORT_VERSION: "2024.7"
|
||||||
DEFAULT_PYTHON: "3.12"
|
DEFAULT_PYTHON: "3.12"
|
||||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
@ -89,7 +89,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Generate partial Python venv restore key
|
- name: Generate partial Python venv restore key
|
||||||
id: generate_python_cache_key
|
id: generate_python_cache_key
|
||||||
run: |
|
run: |
|
||||||
@ -226,7 +226,7 @@ jobs:
|
|||||||
- info
|
- info
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
@ -272,7 +272,7 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
id: python
|
id: python
|
||||||
@ -312,7 +312,7 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
id: python
|
id: python
|
||||||
@ -351,7 +351,7 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
id: python
|
id: python
|
||||||
@ -445,7 +445,7 @@ jobs:
|
|||||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
@ -522,7 +522,7 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
@ -554,7 +554,7 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
@ -587,7 +587,7 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
@ -611,14 +611,59 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python --version
|
python --version
|
||||||
pylint --ignore-missing-annotations=y --ignore-wrong-coordinator-module=y homeassistant
|
pylint --ignore-missing-annotations=y homeassistant
|
||||||
- name: Run pylint (partially)
|
- name: Run pylint (partially)
|
||||||
if: needs.info.outputs.test_full_suite == 'false'
|
if: needs.info.outputs.test_full_suite == 'false'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
python --version
|
python --version
|
||||||
pylint --ignore-missing-annotations=y --ignore-wrong-coordinator-module=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
||||||
|
|
||||||
|
pylint-tests:
|
||||||
|
name: Check pylint on tests
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 20
|
||||||
|
if: |
|
||||||
|
(github.event.inputs.mypy-only != 'true' || github.event.inputs.pylint-only == 'true')
|
||||||
|
&& (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true')
|
||||||
|
needs:
|
||||||
|
- info
|
||||||
|
- base
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v4.1.7
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
id: python
|
||||||
|
uses: actions/setup-python@v5.1.0
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
check-latest: true
|
||||||
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache/restore@v4.0.2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
fail-on-cache-miss: true
|
||||||
|
key: >-
|
||||||
|
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
|
needs.info.outputs.python_cache_key }}
|
||||||
|
- name: Register pylint problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/pylint.json"
|
||||||
|
- name: Run pylint (fully)
|
||||||
|
if: needs.info.outputs.test_full_suite == 'true'
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
python --version
|
||||||
|
pylint --ignore-missing-annotations=y tests
|
||||||
|
- 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 tests/components/${{ needs.info.outputs.tests_glob }}
|
||||||
|
|
||||||
mypy:
|
mypy:
|
||||||
name: Check mypy
|
name: Check mypy
|
||||||
@ -631,7 +676,7 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
@ -704,7 +749,7 @@ jobs:
|
|||||||
ffmpeg \
|
ffmpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
@ -746,6 +791,7 @@ jobs:
|
|||||||
- hassfest
|
- hassfest
|
||||||
- lint-other
|
- lint-other
|
||||||
- lint-ruff
|
- lint-ruff
|
||||||
|
- lint-ruff-format
|
||||||
- mypy
|
- mypy
|
||||||
- prepare-pytest-full
|
- prepare-pytest-full
|
||||||
strategy:
|
strategy:
|
||||||
@ -765,7 +811,7 @@ jobs:
|
|||||||
ffmpeg \
|
ffmpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
@ -863,6 +909,7 @@ jobs:
|
|||||||
- hassfest
|
- hassfest
|
||||||
- lint-other
|
- lint-other
|
||||||
- lint-ruff
|
- lint-ruff
|
||||||
|
- lint-ruff-format
|
||||||
- mypy
|
- mypy
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -881,7 +928,7 @@ jobs:
|
|||||||
ffmpeg \
|
ffmpeg \
|
||||||
libmariadb-dev-compat
|
libmariadb-dev-compat
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
@ -986,6 +1033,7 @@ jobs:
|
|||||||
- hassfest
|
- hassfest
|
||||||
- lint-other
|
- lint-other
|
||||||
- lint-ruff
|
- lint-ruff
|
||||||
|
- lint-ruff-format
|
||||||
- mypy
|
- mypy
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -1004,7 +1052,7 @@ jobs:
|
|||||||
ffmpeg \
|
ffmpeg \
|
||||||
postgresql-server-dev-14
|
postgresql-server-dev-14
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
@ -1099,18 +1147,19 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.7
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'true'
|
if: needs.info.outputs.test_full_suite == 'true'
|
||||||
uses: codecov/codecov-action@v4.3.1
|
uses: codecov/codecov-action@v4.5.0
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
flags: full-suite
|
flags: full-suite
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
version: v0.6.0
|
||||||
|
|
||||||
pytest-partial:
|
pytest-partial:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
@ -1128,6 +1177,7 @@ jobs:
|
|||||||
- hassfest
|
- hassfest
|
||||||
- lint-other
|
- lint-other
|
||||||
- lint-ruff
|
- lint-ruff
|
||||||
|
- lint-ruff-format
|
||||||
- mypy
|
- mypy
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -1146,7 +1196,7 @@ jobs:
|
|||||||
ffmpeg \
|
ffmpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
@ -1233,14 +1283,15 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.7
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'false'
|
if: needs.info.outputs.test_full_suite == 'false'
|
||||||
uses: codecov/codecov-action@v4.3.1
|
uses: codecov/codecov-action@v4.5.0
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
version: v0.6.0
|
||||||
|
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -21,14 +21,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.25.3
|
uses: github/codeql-action/init@v3.25.10
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.25.3
|
uses: github/codeql-action/analyze@v3.25.10
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@ -10,7 +10,7 @@ on:
|
|||||||
- "**strings.json"
|
- "**strings.json"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEFAULT_PYTHON: "3.11"
|
DEFAULT_PYTHON: "3.12"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
upload:
|
upload:
|
||||||
@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.1.0
|
||||||
|
6
.github/workflows/wheels.yml
vendored
6
.github/workflows/wheels.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
|||||||
architectures: ${{ steps.info.outputs.architectures }}
|
architectures: ${{ steps.info.outputs.architectures }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
@ -118,7 +118,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.7
|
||||||
@ -156,7 +156,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.4
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.1.7
|
uses: actions/download-artifact@v4.1.7
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -34,6 +34,7 @@ Icon
|
|||||||
|
|
||||||
# GITHUB Proposed Python stuff:
|
# GITHUB Proposed Python stuff:
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
__pycache__
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.4.3
|
rev: v0.4.9
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
@ -8,11 +8,11 @@ repos:
|
|||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$
|
files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.(py|pyi)$
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.2.6
|
rev: v2.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
args:
|
args:
|
||||||
- --ignore-words-list=additionals,alle,alot,astroid,bund,caf,convencional,currenty,datas,farenheit,falsy,fo,frequence,haa,hass,iif,incomfort,ines,ist,nam,nd,pres,pullrequests,resset,rime,ser,serie,te,technik,ue,unsecure,vor,withing,zar
|
- --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn,pres,ser,ue
|
||||||
- --skip="./.*,*.csv,*.json,*.ambr"
|
- --skip="./.*,*.csv,*.json,*.ambr"
|
||||||
- --quiet-level=2
|
- --quiet-level=2
|
||||||
exclude_types: [csv, json, html]
|
exclude_types: [csv, json, html]
|
||||||
@ -61,15 +61,15 @@ repos:
|
|||||||
name: mypy
|
name: mypy
|
||||||
entry: script/run-in-env.sh mypy
|
entry: script/run-in-env.sh mypy
|
||||||
language: script
|
language: script
|
||||||
types: [python]
|
types_or: [python, pyi]
|
||||||
require_serial: true
|
require_serial: true
|
||||||
files: ^(homeassistant|pylint)/.+\.(py|pyi)$
|
files: ^(homeassistant|pylint)/.+\.(py|pyi)$
|
||||||
- id: pylint
|
- id: pylint
|
||||||
name: pylint
|
name: pylint
|
||||||
entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y
|
entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y
|
||||||
language: script
|
language: script
|
||||||
types: [python]
|
types_or: [python, pyi]
|
||||||
files: ^homeassistant/.+\.py$
|
files: ^(homeassistant|tests)/.+\.(py|pyi)$
|
||||||
- id: gen_requirements_all
|
- id: gen_requirements_all
|
||||||
name: gen_requirements_all
|
name: gen_requirements_all
|
||||||
entry: script/run-in-env.sh python3 -m script.gen_requirements_all
|
entry: script/run-in-env.sh python3 -m script.gen_requirements_all
|
||||||
|
@ -48,6 +48,7 @@ homeassistant.components.adax.*
|
|||||||
homeassistant.components.adguard.*
|
homeassistant.components.adguard.*
|
||||||
homeassistant.components.aftership.*
|
homeassistant.components.aftership.*
|
||||||
homeassistant.components.air_quality.*
|
homeassistant.components.air_quality.*
|
||||||
|
homeassistant.components.airgradient.*
|
||||||
homeassistant.components.airly.*
|
homeassistant.components.airly.*
|
||||||
homeassistant.components.airnow.*
|
homeassistant.components.airnow.*
|
||||||
homeassistant.components.airq.*
|
homeassistant.components.airq.*
|
||||||
@ -65,7 +66,6 @@ homeassistant.components.alexa.*
|
|||||||
homeassistant.components.alpha_vantage.*
|
homeassistant.components.alpha_vantage.*
|
||||||
homeassistant.components.amazon_polly.*
|
homeassistant.components.amazon_polly.*
|
||||||
homeassistant.components.amberelectric.*
|
homeassistant.components.amberelectric.*
|
||||||
homeassistant.components.ambiclimate.*
|
|
||||||
homeassistant.components.ambient_network.*
|
homeassistant.components.ambient_network.*
|
||||||
homeassistant.components.ambient_station.*
|
homeassistant.components.ambient_station.*
|
||||||
homeassistant.components.amcrest.*
|
homeassistant.components.amcrest.*
|
||||||
@ -84,6 +84,7 @@ homeassistant.components.api.*
|
|||||||
homeassistant.components.apple_tv.*
|
homeassistant.components.apple_tv.*
|
||||||
homeassistant.components.apprise.*
|
homeassistant.components.apprise.*
|
||||||
homeassistant.components.aprs.*
|
homeassistant.components.aprs.*
|
||||||
|
homeassistant.components.apsystems.*
|
||||||
homeassistant.components.aqualogic.*
|
homeassistant.components.aqualogic.*
|
||||||
homeassistant.components.aquostv.*
|
homeassistant.components.aquostv.*
|
||||||
homeassistant.components.aranet.*
|
homeassistant.components.aranet.*
|
||||||
@ -260,6 +261,7 @@ homeassistant.components.jellyfin.*
|
|||||||
homeassistant.components.jewish_calendar.*
|
homeassistant.components.jewish_calendar.*
|
||||||
homeassistant.components.jvc_projector.*
|
homeassistant.components.jvc_projector.*
|
||||||
homeassistant.components.kaleidescape.*
|
homeassistant.components.kaleidescape.*
|
||||||
|
homeassistant.components.knocki.*
|
||||||
homeassistant.components.knx.*
|
homeassistant.components.knx.*
|
||||||
homeassistant.components.kraken.*
|
homeassistant.components.kraken.*
|
||||||
homeassistant.components.lacrosse.*
|
homeassistant.components.lacrosse.*
|
||||||
@ -301,6 +303,7 @@ homeassistant.components.minecraft_server.*
|
|||||||
homeassistant.components.mjpeg.*
|
homeassistant.components.mjpeg.*
|
||||||
homeassistant.components.modbus.*
|
homeassistant.components.modbus.*
|
||||||
homeassistant.components.modem_callerid.*
|
homeassistant.components.modem_callerid.*
|
||||||
|
homeassistant.components.monzo.*
|
||||||
homeassistant.components.moon.*
|
homeassistant.components.moon.*
|
||||||
homeassistant.components.mopeka.*
|
homeassistant.components.mopeka.*
|
||||||
homeassistant.components.motionmount.*
|
homeassistant.components.motionmount.*
|
||||||
@ -339,7 +342,6 @@ homeassistant.components.persistent_notification.*
|
|||||||
homeassistant.components.pi_hole.*
|
homeassistant.components.pi_hole.*
|
||||||
homeassistant.components.ping.*
|
homeassistant.components.ping.*
|
||||||
homeassistant.components.plugwise.*
|
homeassistant.components.plugwise.*
|
||||||
homeassistant.components.poolsense.*
|
|
||||||
homeassistant.components.powerwall.*
|
homeassistant.components.powerwall.*
|
||||||
homeassistant.components.private_ble_device.*
|
homeassistant.components.private_ble_device.*
|
||||||
homeassistant.components.prometheus.*
|
homeassistant.components.prometheus.*
|
||||||
@ -427,6 +429,7 @@ homeassistant.components.tcp.*
|
|||||||
homeassistant.components.technove.*
|
homeassistant.components.technove.*
|
||||||
homeassistant.components.tedee.*
|
homeassistant.components.tedee.*
|
||||||
homeassistant.components.text.*
|
homeassistant.components.text.*
|
||||||
|
homeassistant.components.thethingsnetwork.*
|
||||||
homeassistant.components.threshold.*
|
homeassistant.components.threshold.*
|
||||||
homeassistant.components.tibber.*
|
homeassistant.components.tibber.*
|
||||||
homeassistant.components.tile.*
|
homeassistant.components.tile.*
|
||||||
|
4
.vscode/settings.default.json
vendored
4
.vscode/settings.default.json
vendored
@ -4,5 +4,7 @@
|
|||||||
// https://github.com/microsoft/vscode-python/issues/14067
|
// https://github.com/microsoft/vscode-python/issues/14067
|
||||||
"python.testing.pytestArgs": ["--no-cov"],
|
"python.testing.pytestArgs": ["--no-cov"],
|
||||||
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
|
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
|
||||||
"python.testing.pytestEnabled": false
|
"python.testing.pytestEnabled": false,
|
||||||
|
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
||||||
|
"pylint.importStrategy": "fromEnvironment"
|
||||||
}
|
}
|
||||||
|
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
@ -103,7 +103,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Install all Requirements",
|
"label": "Install all Requirements",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pip3 install -r requirements_all.txt",
|
"command": "uv pip install -r requirements_all.txt",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
@ -117,7 +117,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Install all Test Requirements",
|
"label": "Install all Test Requirements",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pip3 install -r requirements_test_all.txt",
|
"command": "uv pip install -r requirements_test_all.txt",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
|
62
CODEOWNERS
62
CODEOWNERS
@ -56,6 +56,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/agent_dvr/ @ispysoftware
|
/tests/components/agent_dvr/ @ispysoftware
|
||||||
/homeassistant/components/air_quality/ @home-assistant/core
|
/homeassistant/components/air_quality/ @home-assistant/core
|
||||||
/tests/components/air_quality/ @home-assistant/core
|
/tests/components/air_quality/ @home-assistant/core
|
||||||
|
/homeassistant/components/airgradient/ @airgradienthq @joostlek
|
||||||
|
/tests/components/airgradient/ @airgradienthq @joostlek
|
||||||
/homeassistant/components/airly/ @bieniu
|
/homeassistant/components/airly/ @bieniu
|
||||||
/tests/components/airly/ @bieniu
|
/tests/components/airly/ @bieniu
|
||||||
/homeassistant/components/airnow/ @asymworks
|
/homeassistant/components/airnow/ @asymworks
|
||||||
@ -78,18 +80,17 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/airzone/ @Noltari
|
/tests/components/airzone/ @Noltari
|
||||||
/homeassistant/components/airzone_cloud/ @Noltari
|
/homeassistant/components/airzone_cloud/ @Noltari
|
||||||
/tests/components/airzone_cloud/ @Noltari
|
/tests/components/airzone_cloud/ @Noltari
|
||||||
/homeassistant/components/aladdin_connect/ @mkmer
|
/homeassistant/components/aladdin_connect/ @swcloudgenie
|
||||||
/tests/components/aladdin_connect/ @mkmer
|
/tests/components/aladdin_connect/ @swcloudgenie
|
||||||
/homeassistant/components/alarm_control_panel/ @home-assistant/core
|
/homeassistant/components/alarm_control_panel/ @home-assistant/core
|
||||||
/tests/components/alarm_control_panel/ @home-assistant/core
|
/tests/components/alarm_control_panel/ @home-assistant/core
|
||||||
/homeassistant/components/alert/ @home-assistant/core @frenck
|
/homeassistant/components/alert/ @home-assistant/core @frenck
|
||||||
/tests/components/alert/ @home-assistant/core @frenck
|
/tests/components/alert/ @home-assistant/core @frenck
|
||||||
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||||
|
/homeassistant/components/amazon_polly/ @jschlyter
|
||||||
/homeassistant/components/amberelectric/ @madpilot
|
/homeassistant/components/amberelectric/ @madpilot
|
||||||
/tests/components/amberelectric/ @madpilot
|
/tests/components/amberelectric/ @madpilot
|
||||||
/homeassistant/components/ambiclimate/ @danielhiversen
|
|
||||||
/tests/components/ambiclimate/ @danielhiversen
|
|
||||||
/homeassistant/components/ambient_network/ @thomaskistler
|
/homeassistant/components/ambient_network/ @thomaskistler
|
||||||
/tests/components/ambient_network/ @thomaskistler
|
/tests/components/ambient_network/ @thomaskistler
|
||||||
/homeassistant/components/ambient_station/ @bachya
|
/homeassistant/components/ambient_station/ @bachya
|
||||||
@ -127,6 +128,10 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/aprilaire/ @chamberlain2007
|
/tests/components/aprilaire/ @chamberlain2007
|
||||||
/homeassistant/components/aprs/ @PhilRW
|
/homeassistant/components/aprs/ @PhilRW
|
||||||
/tests/components/aprs/ @PhilRW
|
/tests/components/aprs/ @PhilRW
|
||||||
|
/homeassistant/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
|
||||||
|
/tests/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
|
||||||
|
/homeassistant/components/aquacell/ @Jordi1990
|
||||||
|
/tests/components/aquacell/ @Jordi1990
|
||||||
/homeassistant/components/aranet/ @aschmitz @thecode @anrijs
|
/homeassistant/components/aranet/ @aschmitz @thecode @anrijs
|
||||||
/tests/components/aranet/ @aschmitz @thecode @anrijs
|
/tests/components/aranet/ @aschmitz @thecode @anrijs
|
||||||
/homeassistant/components/arcam_fmj/ @elupus
|
/homeassistant/components/arcam_fmj/ @elupus
|
||||||
@ -161,6 +166,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/awair/ @ahayworth @danielsjf
|
/tests/components/awair/ @ahayworth @danielsjf
|
||||||
/homeassistant/components/axis/ @Kane610
|
/homeassistant/components/axis/ @Kane610
|
||||||
/tests/components/axis/ @Kane610
|
/tests/components/axis/ @Kane610
|
||||||
|
/homeassistant/components/azure_data_explorer/ @kaareseras
|
||||||
|
/tests/components/azure_data_explorer/ @kaareseras
|
||||||
/homeassistant/components/azure_devops/ @timmo001
|
/homeassistant/components/azure_devops/ @timmo001
|
||||||
/tests/components/azure_devops/ @timmo001
|
/tests/components/azure_devops/ @timmo001
|
||||||
/homeassistant/components/azure_event_hub/ @eavanvalkenburg
|
/homeassistant/components/azure_event_hub/ @eavanvalkenburg
|
||||||
@ -180,8 +187,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/binary_sensor/ @home-assistant/core
|
/homeassistant/components/binary_sensor/ @home-assistant/core
|
||||||
/tests/components/binary_sensor/ @home-assistant/core
|
/tests/components/binary_sensor/ @home-assistant/core
|
||||||
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
||||||
/homeassistant/components/blebox/ @bbx-a @riokuu @swistakm
|
/homeassistant/components/blebox/ @bbx-a @swistakm
|
||||||
/tests/components/blebox/ @bbx-a @riokuu @swistakm
|
/tests/components/blebox/ @bbx-a @swistakm
|
||||||
/homeassistant/components/blink/ @fronzbot @mkmer
|
/homeassistant/components/blink/ @fronzbot @mkmer
|
||||||
/tests/components/blink/ @fronzbot @mkmer
|
/tests/components/blink/ @fronzbot @mkmer
|
||||||
/homeassistant/components/blue_current/ @Floris272 @gleeuwen
|
/homeassistant/components/blue_current/ @Floris272 @gleeuwen
|
||||||
@ -232,7 +239,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ccm15/ @ocalvo
|
/tests/components/ccm15/ @ocalvo
|
||||||
/homeassistant/components/cert_expiry/ @jjlawren
|
/homeassistant/components/cert_expiry/ @jjlawren
|
||||||
/tests/components/cert_expiry/ @jjlawren
|
/tests/components/cert_expiry/ @jjlawren
|
||||||
/homeassistant/components/circuit/ @braam
|
|
||||||
/homeassistant/components/cisco_ios/ @fbradyirl
|
/homeassistant/components/cisco_ios/ @fbradyirl
|
||||||
/homeassistant/components/cisco_mobility_express/ @fbradyirl
|
/homeassistant/components/cisco_mobility_express/ @fbradyirl
|
||||||
/homeassistant/components/cisco_webex_teams/ @fbradyirl
|
/homeassistant/components/cisco_webex_teams/ @fbradyirl
|
||||||
@ -338,8 +344,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||||
/homeassistant/components/dsmr/ @Robbie1221 @frenck
|
/homeassistant/components/dsmr/ @Robbie1221 @frenck
|
||||||
/tests/components/dsmr/ @Robbie1221 @frenck
|
/tests/components/dsmr/ @Robbie1221 @frenck
|
||||||
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox
|
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||||
/tests/components/dsmr_reader/ @sorted-bits @glodenox
|
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||||
/homeassistant/components/duotecno/ @cereal2nd
|
/homeassistant/components/duotecno/ @cereal2nd
|
||||||
/tests/components/duotecno/ @cereal2nd
|
/tests/components/duotecno/ @cereal2nd
|
||||||
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
|
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
|
||||||
@ -375,7 +381,7 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/elvia/ @ludeeus
|
/homeassistant/components/elvia/ @ludeeus
|
||||||
/tests/components/elvia/ @ludeeus
|
/tests/components/elvia/ @ludeeus
|
||||||
/homeassistant/components/emby/ @mezz64
|
/homeassistant/components/emby/ @mezz64
|
||||||
/homeassistant/components/emoncms/ @borpin
|
/homeassistant/components/emoncms/ @borpin @alexandrecuer
|
||||||
/homeassistant/components/emonitor/ @bdraco
|
/homeassistant/components/emonitor/ @bdraco
|
||||||
/tests/components/emonitor/ @bdraco
|
/tests/components/emonitor/ @bdraco
|
||||||
/homeassistant/components/emulated_hue/ @bdraco @Tho85
|
/homeassistant/components/emulated_hue/ @bdraco @Tho85
|
||||||
@ -654,7 +660,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/imgw_pib/ @bieniu
|
/tests/components/imgw_pib/ @bieniu
|
||||||
/homeassistant/components/improv_ble/ @emontnemery
|
/homeassistant/components/improv_ble/ @emontnemery
|
||||||
/tests/components/improv_ble/ @emontnemery
|
/tests/components/improv_ble/ @emontnemery
|
||||||
/homeassistant/components/incomfort/ @zxdavb
|
/homeassistant/components/incomfort/ @jbouwh
|
||||||
|
/tests/components/incomfort/ @jbouwh
|
||||||
/homeassistant/components/influxdb/ @mdegat01
|
/homeassistant/components/influxdb/ @mdegat01
|
||||||
/tests/components/influxdb/ @mdegat01
|
/tests/components/influxdb/ @mdegat01
|
||||||
/homeassistant/components/inkbird/ @bdraco
|
/homeassistant/components/inkbird/ @bdraco
|
||||||
@ -692,10 +699,14 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/iqvia/ @bachya
|
/homeassistant/components/iqvia/ @bachya
|
||||||
/tests/components/iqvia/ @bachya
|
/tests/components/iqvia/ @bachya
|
||||||
/homeassistant/components/irish_rail_transport/ @ttroy50
|
/homeassistant/components/irish_rail_transport/ @ttroy50
|
||||||
|
/homeassistant/components/isal/ @bdraco
|
||||||
|
/tests/components/isal/ @bdraco
|
||||||
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||||
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
|
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||||
/homeassistant/components/iss/ @DurgNomis-drol
|
/homeassistant/components/iss/ @DurgNomis-drol
|
||||||
/tests/components/iss/ @DurgNomis-drol
|
/tests/components/iss/ @DurgNomis-drol
|
||||||
|
/homeassistant/components/ista_ecotrend/ @tr4nt0r
|
||||||
|
/tests/components/ista_ecotrend/ @tr4nt0r
|
||||||
/homeassistant/components/isy994/ @bdraco @shbatm
|
/homeassistant/components/isy994/ @bdraco @shbatm
|
||||||
/tests/components/isy994/ @bdraco @shbatm
|
/tests/components/isy994/ @bdraco @shbatm
|
||||||
/homeassistant/components/izone/ @Swamp-Ig
|
/homeassistant/components/izone/ @Swamp-Ig
|
||||||
@ -726,6 +737,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/kitchen_sink/ @home-assistant/core
|
/tests/components/kitchen_sink/ @home-assistant/core
|
||||||
/homeassistant/components/kmtronic/ @dgomes
|
/homeassistant/components/kmtronic/ @dgomes
|
||||||
/tests/components/kmtronic/ @dgomes
|
/tests/components/kmtronic/ @dgomes
|
||||||
|
/homeassistant/components/knocki/ @joostlek @jgatto1
|
||||||
|
/tests/components/knocki/ @joostlek @jgatto1
|
||||||
/homeassistant/components/knx/ @Julius2342 @farmio @marvin-w
|
/homeassistant/components/knx/ @Julius2342 @farmio @marvin-w
|
||||||
/tests/components/knx/ @Julius2342 @farmio @marvin-w
|
/tests/components/knx/ @Julius2342 @farmio @marvin-w
|
||||||
/homeassistant/components/kodi/ @OnFreund
|
/homeassistant/components/kodi/ @OnFreund
|
||||||
@ -815,6 +828,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/matrix/ @PaarthShah
|
/tests/components/matrix/ @PaarthShah
|
||||||
/homeassistant/components/matter/ @home-assistant/matter
|
/homeassistant/components/matter/ @home-assistant/matter
|
||||||
/tests/components/matter/ @home-assistant/matter
|
/tests/components/matter/ @home-assistant/matter
|
||||||
|
/homeassistant/components/mealie/ @joostlek
|
||||||
|
/tests/components/mealie/ @joostlek
|
||||||
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
||||||
/tests/components/meater/ @Sotolotl @emontnemery
|
/tests/components/meater/ @Sotolotl @emontnemery
|
||||||
/homeassistant/components/medcom_ble/ @elafargue
|
/homeassistant/components/medcom_ble/ @elafargue
|
||||||
@ -826,6 +841,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/media_source/ @hunterjm
|
/homeassistant/components/media_source/ @hunterjm
|
||||||
/tests/components/media_source/ @hunterjm
|
/tests/components/media_source/ @hunterjm
|
||||||
/homeassistant/components/mediaroom/ @dgomes
|
/homeassistant/components/mediaroom/ @dgomes
|
||||||
|
/homeassistant/components/melcloud/ @erwindouna
|
||||||
|
/tests/components/melcloud/ @erwindouna
|
||||||
/homeassistant/components/melissa/ @kennedyshead
|
/homeassistant/components/melissa/ @kennedyshead
|
||||||
/tests/components/melissa/ @kennedyshead
|
/tests/components/melissa/ @kennedyshead
|
||||||
/homeassistant/components/melnor/ @vanstinator
|
/homeassistant/components/melnor/ @vanstinator
|
||||||
@ -867,6 +884,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
||||||
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
||||||
/tests/components/monoprice/ @etsinko @OnFreund
|
/tests/components/monoprice/ @etsinko @OnFreund
|
||||||
|
/homeassistant/components/monzo/ @jakemartin-icl
|
||||||
|
/tests/components/monzo/ @jakemartin-icl
|
||||||
/homeassistant/components/moon/ @fabaff @frenck
|
/homeassistant/components/moon/ @fabaff @frenck
|
||||||
/tests/components/moon/ @fabaff @frenck
|
/tests/components/moon/ @fabaff @frenck
|
||||||
/homeassistant/components/mopeka/ @bdraco
|
/homeassistant/components/mopeka/ @bdraco
|
||||||
@ -896,8 +915,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/myuplink/ @pajzo @astrandb
|
/tests/components/myuplink/ @pajzo @astrandb
|
||||||
/homeassistant/components/nam/ @bieniu
|
/homeassistant/components/nam/ @bieniu
|
||||||
/tests/components/nam/ @bieniu
|
/tests/components/nam/ @bieniu
|
||||||
/homeassistant/components/nanoleaf/ @milanmeu
|
/homeassistant/components/nanoleaf/ @milanmeu @joostlek
|
||||||
/tests/components/nanoleaf/ @milanmeu
|
/tests/components/nanoleaf/ @milanmeu @joostlek
|
||||||
/homeassistant/components/neato/ @Santobert
|
/homeassistant/components/neato/ @Santobert
|
||||||
/tests/components/neato/ @Santobert
|
/tests/components/neato/ @Santobert
|
||||||
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM
|
/homeassistant/components/nederlandse_spoorwegen/ @YarmoM
|
||||||
@ -1086,6 +1105,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/pvoutput/ @frenck
|
/tests/components/pvoutput/ @frenck
|
||||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue
|
/homeassistant/components/pvpc_hourly_pricing/ @azogue
|
||||||
/tests/components/pvpc_hourly_pricing/ @azogue
|
/tests/components/pvpc_hourly_pricing/ @azogue
|
||||||
|
/homeassistant/components/pyload/ @tr4nt0r
|
||||||
|
/tests/components/pyload/ @tr4nt0r
|
||||||
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
|
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||||
/tests/components/qbittorrent/ @geoffreylagaisse @finder39
|
/tests/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||||
/homeassistant/components/qingping/ @bdraco
|
/homeassistant/components/qingping/ @bdraco
|
||||||
@ -1273,8 +1294,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/smappee/ @bsmappee
|
/tests/components/smappee/ @bsmappee
|
||||||
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
||||||
/tests/components/smart_meter_texas/ @grahamwetzler
|
/tests/components/smart_meter_texas/ @grahamwetzler
|
||||||
/homeassistant/components/smartthings/ @andrewsayre
|
|
||||||
/tests/components/smartthings/ @andrewsayre
|
|
||||||
/homeassistant/components/smarttub/ @mdz
|
/homeassistant/components/smarttub/ @mdz
|
||||||
/tests/components/smarttub/ @mdz
|
/tests/components/smarttub/ @mdz
|
||||||
/homeassistant/components/smarty/ @z0mbieprocess
|
/homeassistant/components/smarty/ @z0mbieprocess
|
||||||
@ -1291,8 +1310,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/solaredge/ @frenck @bdraco
|
/homeassistant/components/solaredge/ @frenck @bdraco
|
||||||
/tests/components/solaredge/ @frenck @bdraco
|
/tests/components/solaredge/ @frenck @bdraco
|
||||||
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
||||||
/homeassistant/components/solarlog/ @Ernst79
|
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
|
||||||
/tests/components/solarlog/ @Ernst79
|
/tests/components/solarlog/ @Ernst79 @dontinelli
|
||||||
/homeassistant/components/solax/ @squishykid
|
/homeassistant/components/solax/ @squishykid
|
||||||
/tests/components/solax/ @squishykid
|
/tests/components/solax/ @squishykid
|
||||||
/homeassistant/components/soma/ @ratsept @sebfortier2288
|
/homeassistant/components/soma/ @ratsept @sebfortier2288
|
||||||
@ -1361,8 +1380,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/switchbee/ @jafar-atili
|
/tests/components/switchbee/ @jafar-atili
|
||||||
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||||
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||||
/homeassistant/components/switchbot_cloud/ @SeraphicRav
|
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland
|
||||||
/tests/components/switchbot_cloud/ @SeraphicRav
|
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland
|
||||||
/homeassistant/components/switcher_kis/ @thecode
|
/homeassistant/components/switcher_kis/ @thecode
|
||||||
/tests/components/switcher_kis/ @thecode
|
/tests/components/switcher_kis/ @thecode
|
||||||
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
|
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
|
||||||
@ -1415,7 +1434,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/thermobeacon/ @bdraco
|
/tests/components/thermobeacon/ @bdraco
|
||||||
/homeassistant/components/thermopro/ @bdraco @h3ss
|
/homeassistant/components/thermopro/ @bdraco @h3ss
|
||||||
/tests/components/thermopro/ @bdraco @h3ss
|
/tests/components/thermopro/ @bdraco @h3ss
|
||||||
/homeassistant/components/thethingsnetwork/ @fabaff
|
/homeassistant/components/thethingsnetwork/ @angelnu
|
||||||
|
/tests/components/thethingsnetwork/ @angelnu
|
||||||
/homeassistant/components/thread/ @home-assistant/core
|
/homeassistant/components/thread/ @home-assistant/core
|
||||||
/tests/components/thread/ @home-assistant/core
|
/tests/components/thread/ @home-assistant/core
|
||||||
/homeassistant/components/tibber/ @danielhiversen
|
/homeassistant/components/tibber/ @danielhiversen
|
||||||
@ -1479,8 +1499,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/unifi/ @Kane610
|
/tests/components/unifi/ @Kane610
|
||||||
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
||||||
/homeassistant/components/unifiled/ @florisvdk
|
/homeassistant/components/unifiled/ @florisvdk
|
||||||
/homeassistant/components/unifiprotect/ @AngellusMortis @bdraco
|
|
||||||
/tests/components/unifiprotect/ @AngellusMortis @bdraco
|
|
||||||
/homeassistant/components/upb/ @gwww
|
/homeassistant/components/upb/ @gwww
|
||||||
/tests/components/upb/ @gwww
|
/tests/components/upb/ @gwww
|
||||||
/homeassistant/components/upc_connect/ @pvizeli @fabaff
|
/homeassistant/components/upc_connect/ @pvizeli @fabaff
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
We as members, contributors, and leaders pledge to make participation in our
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
identity and expression, level of experience, education, socioeconomic status,
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
and orientation.
|
and orientation.
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ ENV \
|
|||||||
ARG QEMU_CPU
|
ARG QEMU_CPU
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
RUN pip3 install uv==0.1.39
|
RUN pip3 install uv==0.2.13
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
@ -35,21 +35,30 @@ RUN \
|
|||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install uv
|
||||||
|
RUN pip3 install uv
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
# Setup hass-release
|
# Setup hass-release
|
||||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
||||||
&& pip3 install -e hass-release/
|
&& uv pip install --system -e hass-release/
|
||||||
|
|
||||||
WORKDIR /workspaces
|
USER vscode
|
||||||
|
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||||
|
RUN uv venv $VIRTUAL_ENV
|
||||||
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /tmp
|
||||||
|
|
||||||
# Install Python dependencies from requirements
|
# Install Python dependencies from requirements
|
||||||
COPY requirements.txt ./
|
COPY requirements.txt ./
|
||||||
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
||||||
RUN pip3 install -r requirements.txt
|
RUN uv pip install -r requirements.txt
|
||||||
COPY requirements_test.txt requirements_test_pre_commit.txt ./
|
COPY requirements_test.txt requirements_test_pre_commit.txt ./
|
||||||
RUN pip3 install -r requirements_test.txt
|
RUN uv pip install -r requirements_test.txt
|
||||||
RUN rm -rf requirements.txt requirements_test.txt requirements_test_pre_commit.txt homeassistant/
|
|
||||||
|
WORKDIR /workspaces
|
||||||
|
|
||||||
# Set the default shell to bash instead of sh
|
# Set the default shell to bash instead of sh
|
||||||
ENV SHELL /bin/bash
|
ENV SHELL /bin/bash
|
||||||
|
10
build.yaml
10
build.yaml
@ -1,10 +1,10 @@
|
|||||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.03.0
|
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.06.1
|
||||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.03.0
|
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.06.1
|
||||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.03.0
|
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.06.1
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.03.0
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.06.1
|
||||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.03.0
|
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.06.1
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
from contextlib import suppress
|
||||||
import faulthandler
|
import faulthandler
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@ -208,8 +209,10 @@ def main() -> int:
|
|||||||
exit_code = runner.run(runtime_conf)
|
exit_code = runner.run(runtime_conf)
|
||||||
faulthandler.disable()
|
faulthandler.disable()
|
||||||
|
|
||||||
if os.path.getsize(fault_file_name) == 0:
|
# It's possible for the fault file to disappear, so suppress obvious errors
|
||||||
os.remove(fault_file_name)
|
with suppress(FileNotFoundError):
|
||||||
|
if os.path.getsize(fault_file_name) == 0:
|
||||||
|
os.remove(fault_file_name)
|
||||||
|
|
||||||
check_threads()
|
check_threads()
|
||||||
|
|
||||||
|
@ -28,15 +28,14 @@ from .const import ACCESS_TOKEN_EXPIRATION, GROUP_ID_ADMIN, REFRESH_TOKEN_EXPIRA
|
|||||||
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
|
from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config
|
||||||
from .models import AuthFlowResult
|
from .models import AuthFlowResult
|
||||||
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
from .providers import AuthProvider, LoginFlow, auth_provider_from_config
|
||||||
from .session import SessionManager
|
|
||||||
|
|
||||||
EVENT_USER_ADDED = "user_added"
|
EVENT_USER_ADDED = "user_added"
|
||||||
EVENT_USER_UPDATED = "user_updated"
|
EVENT_USER_UPDATED = "user_updated"
|
||||||
EVENT_USER_REMOVED = "user_removed"
|
EVENT_USER_REMOVED = "user_removed"
|
||||||
|
|
||||||
_MfaModuleDict = dict[str, MultiFactorAuthModule]
|
type _MfaModuleDict = dict[str, MultiFactorAuthModule]
|
||||||
_ProviderKey = tuple[str, str | None]
|
type _ProviderKey = tuple[str, str | None]
|
||||||
_ProviderDict = dict[_ProviderKey, AuthProvider]
|
type _ProviderDict = dict[_ProviderKey, AuthProvider]
|
||||||
|
|
||||||
|
|
||||||
class InvalidAuthError(Exception):
|
class InvalidAuthError(Exception):
|
||||||
@ -54,7 +53,7 @@ async def auth_manager_from_config(
|
|||||||
) -> AuthManager:
|
) -> AuthManager:
|
||||||
"""Initialize an auth manager from config.
|
"""Initialize an auth manager from config.
|
||||||
|
|
||||||
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
|
CORE_CONFIG_SCHEMA will make sure no duplicated auth providers or
|
||||||
mfa modules exist in configs.
|
mfa modules exist in configs.
|
||||||
"""
|
"""
|
||||||
store = auth_store.AuthStore(hass)
|
store = auth_store.AuthStore(hass)
|
||||||
@ -181,7 +180,6 @@ class AuthManager:
|
|||||||
self._remove_expired_job = HassJob(
|
self._remove_expired_job = HassJob(
|
||||||
self._async_remove_expired_refresh_tokens, job_type=HassJobType.Callback
|
self._async_remove_expired_refresh_tokens, job_type=HassJobType.Callback
|
||||||
)
|
)
|
||||||
self.session = SessionManager(hass, self)
|
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
async def async_setup(self) -> None:
|
||||||
"""Set up the auth manager."""
|
"""Set up the auth manager."""
|
||||||
@ -192,7 +190,6 @@ class AuthManager:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
self._async_track_next_refresh_token_expiration()
|
self._async_track_next_refresh_token_expiration()
|
||||||
await self.session.async_setup()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auth_providers(self) -> list[AuthProvider]:
|
def auth_providers(self) -> list[AuthProvider]:
|
||||||
@ -519,6 +516,13 @@ class AuthManager:
|
|||||||
for revoke_callback in callbacks:
|
for revoke_callback in callbacks:
|
||||||
revoke_callback()
|
revoke_callback()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_set_expiry(
|
||||||
|
self, refresh_token: models.RefreshToken, *, enable_expiry: bool
|
||||||
|
) -> None:
|
||||||
|
"""Enable or disable expiry of a refresh token."""
|
||||||
|
self._store.async_set_expiry(refresh_token, enable_expiry=enable_expiry)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_remove_expired_refresh_tokens(self, _: datetime | None = None) -> None:
|
def _async_remove_expired_refresh_tokens(self, _: datetime | None = None) -> None:
|
||||||
"""Remove expired refresh tokens."""
|
"""Remove expired refresh tokens."""
|
||||||
|
@ -62,6 +62,7 @@ class AuthStore:
|
|||||||
self._store = Store[dict[str, list[dict[str, Any]]]](
|
self._store = Store[dict[str, list[dict[str, Any]]]](
|
||||||
hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
|
hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
|
||||||
)
|
)
|
||||||
|
self._token_id_to_user_id: dict[str, str] = {}
|
||||||
|
|
||||||
async def async_get_groups(self) -> list[models.Group]:
|
async def async_get_groups(self) -> list[models.Group]:
|
||||||
"""Retrieve all users."""
|
"""Retrieve all users."""
|
||||||
@ -135,7 +136,10 @@ class AuthStore:
|
|||||||
|
|
||||||
async def async_remove_user(self, user: models.User) -> None:
|
async def async_remove_user(self, user: models.User) -> None:
|
||||||
"""Remove a user."""
|
"""Remove a user."""
|
||||||
self._users.pop(user.id)
|
user = self._users.pop(user.id)
|
||||||
|
for refresh_token_id in user.refresh_tokens:
|
||||||
|
del self._token_id_to_user_id[refresh_token_id]
|
||||||
|
user.refresh_tokens.clear()
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
|
||||||
async def async_update_user(
|
async def async_update_user(
|
||||||
@ -218,7 +222,9 @@ class AuthStore:
|
|||||||
kwargs["client_icon"] = client_icon
|
kwargs["client_icon"] = client_icon
|
||||||
|
|
||||||
refresh_token = models.RefreshToken(**kwargs)
|
refresh_token = models.RefreshToken(**kwargs)
|
||||||
user.refresh_tokens[refresh_token.id] = refresh_token
|
token_id = refresh_token.id
|
||||||
|
user.refresh_tokens[token_id] = refresh_token
|
||||||
|
self._token_id_to_user_id[token_id] = user.id
|
||||||
|
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
return refresh_token
|
return refresh_token
|
||||||
@ -226,19 +232,17 @@ class AuthStore:
|
|||||||
@callback
|
@callback
|
||||||
def async_remove_refresh_token(self, refresh_token: models.RefreshToken) -> None:
|
def async_remove_refresh_token(self, refresh_token: models.RefreshToken) -> None:
|
||||||
"""Remove a refresh token."""
|
"""Remove a refresh token."""
|
||||||
for user in self._users.values():
|
refresh_token_id = refresh_token.id
|
||||||
if user.refresh_tokens.pop(refresh_token.id, None):
|
if user_id := self._token_id_to_user_id.get(refresh_token_id):
|
||||||
self._async_schedule_save()
|
del self._users[user_id].refresh_tokens[refresh_token_id]
|
||||||
break
|
del self._token_id_to_user_id[refresh_token_id]
|
||||||
|
self._async_schedule_save()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_get_refresh_token(self, token_id: str) -> models.RefreshToken | None:
|
def async_get_refresh_token(self, token_id: str) -> models.RefreshToken | None:
|
||||||
"""Get refresh token by id."""
|
"""Get refresh token by id."""
|
||||||
for user in self._users.values():
|
if user_id := self._token_id_to_user_id.get(token_id):
|
||||||
refresh_token = user.refresh_tokens.get(token_id)
|
return self._users[user_id].refresh_tokens.get(token_id)
|
||||||
if refresh_token is not None:
|
|
||||||
return refresh_token
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -277,6 +281,21 @@ class AuthStore:
|
|||||||
)
|
)
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_set_expiry(
|
||||||
|
self, refresh_token: models.RefreshToken, *, enable_expiry: bool
|
||||||
|
) -> None:
|
||||||
|
"""Enable or disable expiry of a refresh token."""
|
||||||
|
if enable_expiry:
|
||||||
|
if refresh_token.expire_at is None:
|
||||||
|
refresh_token.expire_at = (
|
||||||
|
refresh_token.last_used_at or dt_util.utcnow()
|
||||||
|
).timestamp() + REFRESH_TOKEN_EXPIRATION
|
||||||
|
self._async_schedule_save()
|
||||||
|
else:
|
||||||
|
refresh_token.expire_at = None
|
||||||
|
self._async_schedule_save()
|
||||||
|
|
||||||
async def async_load(self) -> None: # noqa: C901
|
async def async_load(self) -> None: # noqa: C901
|
||||||
"""Load the users."""
|
"""Load the users."""
|
||||||
if self._loaded:
|
if self._loaded:
|
||||||
@ -290,8 +309,6 @@ class AuthStore:
|
|||||||
perm_lookup = PermissionLookup(ent_reg, dev_reg)
|
perm_lookup = PermissionLookup(ent_reg, dev_reg)
|
||||||
self._perm_lookup = perm_lookup
|
self._perm_lookup = perm_lookup
|
||||||
|
|
||||||
now_ts = dt_util.utcnow().timestamp()
|
|
||||||
|
|
||||||
if data is None or not isinstance(data, dict):
|
if data is None or not isinstance(data, dict):
|
||||||
self._set_defaults()
|
self._set_defaults()
|
||||||
return
|
return
|
||||||
@ -445,14 +462,6 @@ class AuthStore:
|
|||||||
else:
|
else:
|
||||||
last_used_at = None
|
last_used_at = None
|
||||||
|
|
||||||
if (
|
|
||||||
expire_at := rt_dict.get("expire_at")
|
|
||||||
) is None and token_type == models.TOKEN_TYPE_NORMAL:
|
|
||||||
if last_used_at:
|
|
||||||
expire_at = last_used_at.timestamp() + REFRESH_TOKEN_EXPIRATION
|
|
||||||
else:
|
|
||||||
expire_at = now_ts + REFRESH_TOKEN_EXPIRATION
|
|
||||||
|
|
||||||
token = models.RefreshToken(
|
token = models.RefreshToken(
|
||||||
id=rt_dict["id"],
|
id=rt_dict["id"],
|
||||||
user=users[rt_dict["user_id"]],
|
user=users[rt_dict["user_id"]],
|
||||||
@ -469,7 +478,7 @@ class AuthStore:
|
|||||||
jwt_key=rt_dict["jwt_key"],
|
jwt_key=rt_dict["jwt_key"],
|
||||||
last_used_at=last_used_at,
|
last_used_at=last_used_at,
|
||||||
last_used_ip=rt_dict.get("last_used_ip"),
|
last_used_ip=rt_dict.get("last_used_ip"),
|
||||||
expire_at=expire_at,
|
expire_at=rt_dict.get("expire_at"),
|
||||||
version=rt_dict.get("version"),
|
version=rt_dict.get("version"),
|
||||||
)
|
)
|
||||||
if "credential_id" in rt_dict:
|
if "credential_id" in rt_dict:
|
||||||
@ -478,9 +487,18 @@ class AuthStore:
|
|||||||
|
|
||||||
self._groups = groups
|
self._groups = groups
|
||||||
self._users = users
|
self._users = users
|
||||||
|
self._build_token_id_to_user_id()
|
||||||
self._async_schedule_save(INITIAL_LOAD_SAVE_DELAY)
|
self._async_schedule_save(INITIAL_LOAD_SAVE_DELAY)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _build_token_id_to_user_id(self) -> None:
|
||||||
|
"""Build a map of token id to user id."""
|
||||||
|
self._token_id_to_user_id = {
|
||||||
|
token_id: user_id
|
||||||
|
for user_id, user in self._users.items()
|
||||||
|
for token_id in user.refresh_tokens
|
||||||
|
}
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_schedule_save(self, delay: float = DEFAULT_SAVE_DELAY) -> None:
|
def _async_schedule_save(self, delay: float = DEFAULT_SAVE_DELAY) -> None:
|
||||||
"""Save users."""
|
"""Save users."""
|
||||||
@ -574,6 +592,7 @@ class AuthStore:
|
|||||||
read_only_group = _system_read_only_group()
|
read_only_group = _system_read_only_group()
|
||||||
groups[read_only_group.id] = read_only_group
|
groups[read_only_group.id] = read_only_group
|
||||||
self._groups = groups
|
self._groups = groups
|
||||||
|
self._build_token_id_to_user_id()
|
||||||
|
|
||||||
|
|
||||||
def _system_admin_group() -> models.Group:
|
def _system_admin_group() -> models.Group:
|
||||||
|
@ -16,6 +16,7 @@ from homeassistant.data_entry_flow import FlowResult
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.importlib import async_import_module
|
from homeassistant.helpers.importlib import async_import_module
|
||||||
from homeassistant.util.decorator import Registry
|
from homeassistant.util.decorator import Registry
|
||||||
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry()
|
MULTI_FACTOR_AUTH_MODULES: Registry[str, type[MultiFactorAuthModule]] = Registry()
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema(
|
|||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
DATA_REQS = "mfa_auth_module_reqs_processed"
|
DATA_REQS: HassKey[set[str]] = HassKey("mfa_auth_module_reqs_processed")
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ class NotifySetting:
|
|||||||
target: str | None = attr.ib(default=None)
|
target: str | None = attr.ib(default=None)
|
||||||
|
|
||||||
|
|
||||||
_UsersDict = dict[str, NotifySetting]
|
type _UsersDict = dict[str, NotifySetting]
|
||||||
|
|
||||||
|
|
||||||
@MULTI_FACTOR_AUTH_MODULES.register("notify")
|
@MULTI_FACTOR_AUTH_MODULES.register("notify")
|
||||||
|
@ -4,17 +4,17 @@ from collections.abc import Mapping
|
|||||||
|
|
||||||
# MyPy doesn't support recursion yet. So writing it out as far as we need.
|
# MyPy doesn't support recursion yet. So writing it out as far as we need.
|
||||||
|
|
||||||
ValueType = (
|
type ValueType = (
|
||||||
# Example: entities.all = { read: true, control: true }
|
# Example: entities.all = { read: true, control: true }
|
||||||
Mapping[str, bool] | bool | None
|
Mapping[str, bool] | bool | None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Example: entities.domains = { light: … }
|
# Example: entities.domains = { light: … }
|
||||||
SubCategoryDict = Mapping[str, ValueType]
|
type SubCategoryDict = Mapping[str, ValueType]
|
||||||
|
|
||||||
SubCategoryType = SubCategoryDict | bool | None
|
type SubCategoryType = SubCategoryDict | bool | None
|
||||||
|
|
||||||
CategoryType = (
|
type CategoryType = (
|
||||||
# Example: entities.domains
|
# Example: entities.domains
|
||||||
Mapping[str, SubCategoryType]
|
Mapping[str, SubCategoryType]
|
||||||
# Example: entities.all
|
# Example: entities.all
|
||||||
@ -24,4 +24,4 @@ CategoryType = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Example: { entities: … }
|
# Example: { entities: … }
|
||||||
PolicyType = Mapping[str, CategoryType]
|
type PolicyType = Mapping[str, CategoryType]
|
||||||
|
@ -10,8 +10,8 @@ from .const import SUBCAT_ALL
|
|||||||
from .models import PermissionLookup
|
from .models import PermissionLookup
|
||||||
from .types import CategoryType, SubCategoryDict, ValueType
|
from .types import CategoryType, SubCategoryDict, ValueType
|
||||||
|
|
||||||
LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], ValueType | None]
|
type LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], ValueType | None]
|
||||||
SubCatLookupType = dict[str, LookupFunc]
|
type SubCatLookupType = dict[str, LookupFunc]
|
||||||
|
|
||||||
|
|
||||||
def lookup_all(
|
def lookup_all(
|
||||||
|
@ -17,13 +17,14 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.helpers.importlib import async_import_module
|
from homeassistant.helpers.importlib import async_import_module
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.util.decorator import Registry
|
from homeassistant.util.decorator import Registry
|
||||||
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
from ..auth_store import AuthStore
|
from ..auth_store import AuthStore
|
||||||
from ..const import MFA_SESSION_EXPIRATION
|
from ..const import MFA_SESSION_EXPIRATION
|
||||||
from ..models import AuthFlowResult, Credentials, RefreshToken, User, UserMeta
|
from ..models import AuthFlowResult, Credentials, RefreshToken, User, UserMeta
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DATA_REQS = "auth_prov_reqs_processed"
|
DATA_REQS: HassKey[set[str]] = HassKey("auth_prov_reqs_processed")
|
||||||
|
|
||||||
AUTH_PROVIDERS: Registry[str, type[AuthProvider]] = Registry()
|
AUTH_PROVIDERS: Registry[str, type[AuthProvider]] = Registry()
|
||||||
|
|
||||||
|
@ -1,123 +0,0 @@
|
|||||||
"""Support Legacy API password auth provider.
|
|
||||||
|
|
||||||
It will be removed when auth system production ready
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
import hmac
|
|
||||||
from typing import Any, cast
|
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.core import async_get_hass, callback
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
|
||||||
|
|
||||||
from ..models import AuthFlowResult, Credentials, UserMeta
|
|
||||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
|
||||||
|
|
||||||
AUTH_PROVIDER_TYPE = "legacy_api_password"
|
|
||||||
CONF_API_PASSWORD = "api_password"
|
|
||||||
|
|
||||||
_CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
|
|
||||||
{vol.Required(CONF_API_PASSWORD): cv.string}, extra=vol.PREVENT_EXTRA
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _create_repair_and_validate(config: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
async_create_issue(
|
|
||||||
async_get_hass(),
|
|
||||||
"auth",
|
|
||||||
"deprecated_legacy_api_password",
|
|
||||||
breaks_in_ha_version="2024.6.0",
|
|
||||||
is_fixable=False,
|
|
||||||
severity=IssueSeverity.WARNING,
|
|
||||||
translation_key="deprecated_legacy_api_password",
|
|
||||||
)
|
|
||||||
|
|
||||||
return _CONFIG_SCHEMA(config) # type: ignore[no-any-return]
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = _create_repair_and_validate
|
|
||||||
|
|
||||||
|
|
||||||
LEGACY_USER_NAME = "Legacy API password user"
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidAuthError(HomeAssistantError):
|
|
||||||
"""Raised when submitting invalid authentication."""
|
|
||||||
|
|
||||||
|
|
||||||
@AUTH_PROVIDERS.register(AUTH_PROVIDER_TYPE)
|
|
||||||
class LegacyApiPasswordAuthProvider(AuthProvider):
|
|
||||||
"""An auth provider support legacy api_password."""
|
|
||||||
|
|
||||||
DEFAULT_TITLE = "Legacy API Password"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def api_password(self) -> str:
|
|
||||||
"""Return api_password."""
|
|
||||||
return str(self.config[CONF_API_PASSWORD])
|
|
||||||
|
|
||||||
async def async_login_flow(self, context: dict[str, Any] | None) -> LoginFlow:
|
|
||||||
"""Return a flow to login."""
|
|
||||||
return LegacyLoginFlow(self)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_validate_login(self, password: str) -> None:
|
|
||||||
"""Validate password."""
|
|
||||||
api_password = str(self.config[CONF_API_PASSWORD])
|
|
||||||
|
|
||||||
if not hmac.compare_digest(
|
|
||||||
api_password.encode("utf-8"), password.encode("utf-8")
|
|
||||||
):
|
|
||||||
raise InvalidAuthError
|
|
||||||
|
|
||||||
async def async_get_or_create_credentials(
|
|
||||||
self, flow_result: Mapping[str, str]
|
|
||||||
) -> Credentials:
|
|
||||||
"""Return credentials for this login."""
|
|
||||||
credentials = await self.async_credentials()
|
|
||||||
if credentials:
|
|
||||||
return credentials[0]
|
|
||||||
|
|
||||||
return self.async_create_credentials({})
|
|
||||||
|
|
||||||
async def async_user_meta_for_credentials(
|
|
||||||
self, credentials: Credentials
|
|
||||||
) -> UserMeta:
|
|
||||||
"""Return info for the user.
|
|
||||||
|
|
||||||
Will be used to populate info when creating a new user.
|
|
||||||
"""
|
|
||||||
return UserMeta(name=LEGACY_USER_NAME, is_active=True)
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyLoginFlow(LoginFlow):
|
|
||||||
"""Handler for the login flow."""
|
|
||||||
|
|
||||||
async def async_step_init(
|
|
||||||
self, user_input: dict[str, str] | None = None
|
|
||||||
) -> AuthFlowResult:
|
|
||||||
"""Handle the step of the form."""
|
|
||||||
errors = {}
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
try:
|
|
||||||
cast(
|
|
||||||
LegacyApiPasswordAuthProvider, self._auth_provider
|
|
||||||
).async_validate_login(user_input["password"])
|
|
||||||
except InvalidAuthError:
|
|
||||||
errors["base"] = "invalid_auth"
|
|
||||||
|
|
||||||
if not errors:
|
|
||||||
return await self.async_finish({})
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="init",
|
|
||||||
data_schema=vol.Schema({vol.Required("password"): str}),
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
@ -28,8 +28,8 @@ from .. import InvalidAuthError
|
|||||||
from ..models import AuthFlowResult, Credentials, RefreshToken, UserMeta
|
from ..models import AuthFlowResult, Credentials, RefreshToken, UserMeta
|
||||||
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow
|
||||||
|
|
||||||
IPAddress = IPv4Address | IPv6Address
|
type IPAddress = IPv4Address | IPv6Address
|
||||||
IPNetwork = IPv4Network | IPv6Network
|
type IPNetwork = IPv4Network | IPv6Network
|
||||||
|
|
||||||
CONF_TRUSTED_NETWORKS = "trusted_networks"
|
CONF_TRUSTED_NETWORKS = "trusted_networks"
|
||||||
CONF_TRUSTED_USERS = "trusted_users"
|
CONF_TRUSTED_USERS = "trusted_users"
|
||||||
|
@ -1,205 +0,0 @@
|
|||||||
"""Session auth module."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import secrets
|
|
||||||
from typing import TYPE_CHECKING, Final, TypedDict
|
|
||||||
|
|
||||||
from aiohttp.web import Request
|
|
||||||
from aiohttp_session import Session, get_session, new_session
|
|
||||||
from cryptography.fernet import Fernet
|
|
||||||
|
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
|
||||||
from homeassistant.helpers.event import async_call_later
|
|
||||||
from homeassistant.helpers.storage import Store
|
|
||||||
from homeassistant.util import dt as dt_util
|
|
||||||
|
|
||||||
from .models import RefreshToken
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from . import AuthManager
|
|
||||||
|
|
||||||
|
|
||||||
TEMP_TIMEOUT = timedelta(minutes=5)
|
|
||||||
TEMP_TIMEOUT_SECONDS = TEMP_TIMEOUT.total_seconds()
|
|
||||||
|
|
||||||
SESSION_ID = "id"
|
|
||||||
STORAGE_VERSION = 1
|
|
||||||
STORAGE_KEY = "auth.session"
|
|
||||||
|
|
||||||
|
|
||||||
class StrictConnectionTempSessionData:
|
|
||||||
"""Data for accessing unauthorized resources for a short period of time."""
|
|
||||||
|
|
||||||
__slots__ = ("cancel_remove", "absolute_expiry")
|
|
||||||
|
|
||||||
def __init__(self, cancel_remove: CALLBACK_TYPE) -> None:
|
|
||||||
"""Initialize the temp session data."""
|
|
||||||
self.cancel_remove: Final[CALLBACK_TYPE] = cancel_remove
|
|
||||||
self.absolute_expiry: Final[datetime] = dt_util.utcnow() + TEMP_TIMEOUT
|
|
||||||
|
|
||||||
|
|
||||||
class StoreData(TypedDict):
|
|
||||||
"""Data to store."""
|
|
||||||
|
|
||||||
unauthorized_sessions: dict[str, str]
|
|
||||||
key: str
|
|
||||||
|
|
||||||
|
|
||||||
class SessionManager:
|
|
||||||
"""Session manager."""
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, auth: AuthManager) -> None:
|
|
||||||
"""Initialize the strict connection manager."""
|
|
||||||
self._auth = auth
|
|
||||||
self._hass = hass
|
|
||||||
self._temp_sessions: dict[str, StrictConnectionTempSessionData] = {}
|
|
||||||
self._strict_connection_sessions: dict[str, str] = {}
|
|
||||||
self._store = Store[StoreData](
|
|
||||||
hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True
|
|
||||||
)
|
|
||||||
self._key: str | None = None
|
|
||||||
self._refresh_token_revoke_callbacks: dict[str, CALLBACK_TYPE] = {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def key(self) -> str:
|
|
||||||
"""Return the encryption key."""
|
|
||||||
if self._key is None:
|
|
||||||
self._key = Fernet.generate_key().decode()
|
|
||||||
self._async_schedule_save()
|
|
||||||
return self._key
|
|
||||||
|
|
||||||
async def async_validate_request_for_strict_connection_session(
|
|
||||||
self,
|
|
||||||
request: Request,
|
|
||||||
) -> bool:
|
|
||||||
"""Check if a request has a valid strict connection session."""
|
|
||||||
session = await get_session(request)
|
|
||||||
if session.new or session.empty:
|
|
||||||
return False
|
|
||||||
result = self.async_validate_strict_connection_session(session)
|
|
||||||
if result is False:
|
|
||||||
session.invalidate()
|
|
||||||
return result
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_validate_strict_connection_session(
|
|
||||||
self,
|
|
||||||
session: Session,
|
|
||||||
) -> bool:
|
|
||||||
"""Validate a strict connection session."""
|
|
||||||
if not (session_id := session.get(SESSION_ID)):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if token_id := self._strict_connection_sessions.get(session_id):
|
|
||||||
if self._auth.async_get_refresh_token(token_id):
|
|
||||||
return True
|
|
||||||
# refresh token is invalid, delete entry
|
|
||||||
self._strict_connection_sessions.pop(session_id)
|
|
||||||
self._async_schedule_save()
|
|
||||||
|
|
||||||
if data := self._temp_sessions.get(session_id):
|
|
||||||
if dt_util.utcnow() <= data.absolute_expiry:
|
|
||||||
return True
|
|
||||||
# session expired, delete entry
|
|
||||||
self._temp_sessions.pop(session_id).cancel_remove()
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_register_revoke_token_callback(self, refresh_token_id: str) -> None:
|
|
||||||
"""Register a callback to revoke all sessions for a refresh token."""
|
|
||||||
if refresh_token_id in self._refresh_token_revoke_callbacks:
|
|
||||||
return
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_invalidate_auth_sessions() -> None:
|
|
||||||
"""Invalidate all sessions for a refresh token."""
|
|
||||||
self._strict_connection_sessions = {
|
|
||||||
session_id: token_id
|
|
||||||
for session_id, token_id in self._strict_connection_sessions.items()
|
|
||||||
if token_id != refresh_token_id
|
|
||||||
}
|
|
||||||
self._async_schedule_save()
|
|
||||||
|
|
||||||
self._refresh_token_revoke_callbacks[refresh_token_id] = (
|
|
||||||
self._auth.async_register_revoke_token_callback(
|
|
||||||
refresh_token_id, async_invalidate_auth_sessions
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_create_session(
|
|
||||||
self,
|
|
||||||
request: Request,
|
|
||||||
refresh_token: RefreshToken,
|
|
||||||
) -> None:
|
|
||||||
"""Create new session for given refresh token.
|
|
||||||
|
|
||||||
Caller needs to make sure that the refresh token is valid.
|
|
||||||
By creating a session, we are implicitly revoking all other
|
|
||||||
sessions for the given refresh token as there is one refresh
|
|
||||||
token per device/user case.
|
|
||||||
"""
|
|
||||||
self._strict_connection_sessions = {
|
|
||||||
session_id: token_id
|
|
||||||
for session_id, token_id in self._strict_connection_sessions.items()
|
|
||||||
if token_id != refresh_token.id
|
|
||||||
}
|
|
||||||
|
|
||||||
self._async_register_revoke_token_callback(refresh_token.id)
|
|
||||||
session_id = await self._async_create_new_session(request)
|
|
||||||
self._strict_connection_sessions[session_id] = refresh_token.id
|
|
||||||
self._async_schedule_save()
|
|
||||||
|
|
||||||
async def async_create_temp_unauthorized_session(self, request: Request) -> None:
|
|
||||||
"""Create a temporary unauthorized session."""
|
|
||||||
session_id = await self._async_create_new_session(
|
|
||||||
request, max_age=int(TEMP_TIMEOUT_SECONDS)
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def remove(_: datetime) -> None:
|
|
||||||
self._temp_sessions.pop(session_id, None)
|
|
||||||
|
|
||||||
self._temp_sessions[session_id] = StrictConnectionTempSessionData(
|
|
||||||
async_call_later(self._hass, TEMP_TIMEOUT_SECONDS, remove)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _async_create_new_session(
|
|
||||||
self,
|
|
||||||
request: Request,
|
|
||||||
*,
|
|
||||||
max_age: int | None = None,
|
|
||||||
) -> str:
|
|
||||||
session_id = secrets.token_hex(64)
|
|
||||||
|
|
||||||
session = await new_session(request)
|
|
||||||
session[SESSION_ID] = session_id
|
|
||||||
if max_age is not None:
|
|
||||||
session.max_age = max_age
|
|
||||||
return session_id
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_schedule_save(self, delay: float = 1) -> None:
|
|
||||||
"""Save sessions."""
|
|
||||||
self._store.async_delay_save(self._data_to_save, delay)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _data_to_save(self) -> StoreData:
|
|
||||||
"""Return the data to store."""
|
|
||||||
return StoreData(
|
|
||||||
unauthorized_sessions=self._strict_connection_sessions,
|
|
||||||
key=self.key,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
|
||||||
"""Set up session manager."""
|
|
||||||
data = await self._store.async_load()
|
|
||||||
if data is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._key = data["key"]
|
|
||||||
self._strict_connection_sessions = data["unauthorized_sessions"]
|
|
||||||
for token_id in self._strict_connection_sessions.values():
|
|
||||||
self._async_register_revoke_token_callback(token_id)
|
|
@ -1,9 +1,15 @@
|
|||||||
"""Block blocking calls being done in asyncio."""
|
"""Block blocking calls being done in asyncio."""
|
||||||
|
|
||||||
|
import builtins
|
||||||
|
from collections.abc import Callable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import glob
|
||||||
from http.client import HTTPConnection
|
from http.client import HTTPConnection
|
||||||
import importlib
|
import importlib
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -12,12 +18,21 @@ from .util.loop import protect_loop
|
|||||||
|
|
||||||
_IN_TESTS = "unittest" in sys.modules
|
_IN_TESTS = "unittest" in sys.modules
|
||||||
|
|
||||||
|
ALLOWED_FILE_PREFIXES = ("/proc",)
|
||||||
|
|
||||||
|
|
||||||
def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
||||||
# If the module is already imported, we can ignore it.
|
# If the module is already imported, we can ignore it.
|
||||||
return bool((args := mapped_args.get("args")) and args[0] in sys.modules)
|
return bool((args := mapped_args.get("args")) and args[0] in sys.modules)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_file_allowed(mapped_args: dict[str, Any]) -> bool:
|
||||||
|
# If the file is in /proc we can ignore it.
|
||||||
|
args = mapped_args["args"]
|
||||||
|
path = args[0] if type(args[0]) is str else str(args[0]) # noqa: E721
|
||||||
|
return path.startswith(ALLOWED_FILE_PREFIXES)
|
||||||
|
|
||||||
|
|
||||||
def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
||||||
#
|
#
|
||||||
# Avoid extracting the stack unless we need to since it
|
# Avoid extracting the stack unless we need to since it
|
||||||
@ -25,7 +40,7 @@ def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
|||||||
# I/O and we are trying to avoid blocking calls.
|
# I/O and we are trying to avoid blocking calls.
|
||||||
#
|
#
|
||||||
# frame[0] is us
|
# frame[0] is us
|
||||||
# frame[1] is check_loop
|
# frame[1] is raise_for_blocking_call
|
||||||
# frame[2] is protected_loop_func
|
# frame[2] is protected_loop_func
|
||||||
# frame[3] is the offender
|
# frame[3] is the offender
|
||||||
with suppress(ValueError):
|
with suppress(ValueError):
|
||||||
@ -33,28 +48,131 @@ def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class BlockingCall:
|
||||||
|
"""Class to hold information about a blocking call."""
|
||||||
|
|
||||||
|
original_func: Callable
|
||||||
|
object: object
|
||||||
|
function: str
|
||||||
|
check_allowed: Callable[[dict[str, Any]], bool] | None
|
||||||
|
strict: bool
|
||||||
|
strict_core: bool
|
||||||
|
skip_for_tests: bool
|
||||||
|
|
||||||
|
|
||||||
|
_BLOCKING_CALLS: tuple[BlockingCall, ...] = (
|
||||||
|
BlockingCall(
|
||||||
|
original_func=HTTPConnection.putrequest,
|
||||||
|
object=HTTPConnection,
|
||||||
|
function="putrequest",
|
||||||
|
check_allowed=None,
|
||||||
|
strict=True,
|
||||||
|
strict_core=True,
|
||||||
|
skip_for_tests=False,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=time.sleep,
|
||||||
|
object=time,
|
||||||
|
function="sleep",
|
||||||
|
check_allowed=_check_sleep_call_allowed,
|
||||||
|
strict=True,
|
||||||
|
strict_core=True,
|
||||||
|
skip_for_tests=False,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=glob.glob,
|
||||||
|
object=glob,
|
||||||
|
function="glob",
|
||||||
|
check_allowed=None,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=False,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=glob.iglob,
|
||||||
|
object=glob,
|
||||||
|
function="iglob",
|
||||||
|
check_allowed=None,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=False,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=os.walk,
|
||||||
|
object=os,
|
||||||
|
function="walk",
|
||||||
|
check_allowed=None,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=False,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=os.listdir,
|
||||||
|
object=os,
|
||||||
|
function="listdir",
|
||||||
|
check_allowed=None,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=True,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=os.scandir,
|
||||||
|
object=os,
|
||||||
|
function="scandir",
|
||||||
|
check_allowed=None,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=True,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=builtins.open,
|
||||||
|
object=builtins,
|
||||||
|
function="open",
|
||||||
|
check_allowed=_check_file_allowed,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=True,
|
||||||
|
),
|
||||||
|
BlockingCall(
|
||||||
|
original_func=importlib.import_module,
|
||||||
|
object=importlib,
|
||||||
|
function="import_module",
|
||||||
|
check_allowed=_check_import_call_allowed,
|
||||||
|
strict=False,
|
||||||
|
strict_core=False,
|
||||||
|
skip_for_tests=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class BlockedCalls:
|
||||||
|
"""Class to track which calls are blocked."""
|
||||||
|
|
||||||
|
calls: set[BlockingCall]
|
||||||
|
|
||||||
|
|
||||||
|
_BLOCKED_CALLS = BlockedCalls(set())
|
||||||
|
|
||||||
|
|
||||||
def enable() -> None:
|
def enable() -> None:
|
||||||
"""Enable the detection of blocking calls in the event loop."""
|
"""Enable the detection of blocking calls in the event loop."""
|
||||||
# Prevent urllib3 and requests doing I/O in event loop
|
calls = _BLOCKED_CALLS.calls
|
||||||
HTTPConnection.putrequest = protect_loop( # type: ignore[method-assign]
|
if calls:
|
||||||
HTTPConnection.putrequest
|
raise RuntimeError("Blocking call detection is already enabled")
|
||||||
)
|
|
||||||
|
|
||||||
# Prevent sleeping in event loop. Non-strict since 2022.02
|
loop_thread_id = threading.get_ident()
|
||||||
time.sleep = protect_loop(
|
for blocking_call in _BLOCKING_CALLS:
|
||||||
time.sleep, strict=False, check_allowed=_check_sleep_call_allowed
|
if _IN_TESTS and blocking_call.skip_for_tests:
|
||||||
)
|
continue
|
||||||
|
|
||||||
# Currently disabled. pytz doing I/O when getting timezone.
|
protected_function = protect_loop(
|
||||||
# Prevent files being opened inside the event loop
|
blocking_call.original_func,
|
||||||
# builtins.open = protect_loop(builtins.open)
|
strict=blocking_call.strict,
|
||||||
|
strict_core=blocking_call.strict_core,
|
||||||
if not _IN_TESTS:
|
check_allowed=blocking_call.check_allowed,
|
||||||
# unittest uses `importlib.import_module` to do mocking
|
loop_thread_id=loop_thread_id,
|
||||||
# so we cannot protect it if we are running tests
|
|
||||||
importlib.import_module = protect_loop(
|
|
||||||
importlib.import_module,
|
|
||||||
strict_core=False,
|
|
||||||
strict=False,
|
|
||||||
check_allowed=_check_import_call_allowed,
|
|
||||||
)
|
)
|
||||||
|
setattr(blocking_call.object, blocking_call.function, protected_function)
|
||||||
|
calls.add(blocking_call)
|
||||||
|
@ -9,6 +9,7 @@ from functools import partial
|
|||||||
from itertools import chain
|
from itertools import chain
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
|
import mimetypes
|
||||||
from operator import contains, itemgetter
|
from operator import contains, itemgetter
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
@ -62,6 +63,7 @@ from .components import (
|
|||||||
)
|
)
|
||||||
from .components.sensor import recorder as sensor_recorder # noqa: F401
|
from .components.sensor import recorder as sensor_recorder # noqa: F401
|
||||||
from .const import (
|
from .const import (
|
||||||
|
BASE_PLATFORMS,
|
||||||
FORMAT_DATETIME,
|
FORMAT_DATETIME,
|
||||||
KEY_DATA_LOGGING as DATA_LOGGING,
|
KEY_DATA_LOGGING as DATA_LOGGING,
|
||||||
REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
||||||
@ -84,12 +86,11 @@ from .helpers import (
|
|||||||
template,
|
template,
|
||||||
translation,
|
translation,
|
||||||
)
|
)
|
||||||
from .helpers.dispatcher import async_dispatcher_send
|
from .helpers.dispatcher import async_dispatcher_send_internal
|
||||||
from .helpers.storage import get_internal_store_manager
|
from .helpers.storage import get_internal_store_manager
|
||||||
from .helpers.system_info import async_get_system_info
|
from .helpers.system_info import async_get_system_info
|
||||||
from .helpers.typing import ConfigType
|
from .helpers.typing import ConfigType
|
||||||
from .setup import (
|
from .setup import (
|
||||||
BASE_PLATFORMS,
|
|
||||||
# _setup_started is marked as protected to make it clear
|
# _setup_started is marked as protected to make it clear
|
||||||
# that it is not part of the public API and should not be used
|
# that it is not part of the public API and should not be used
|
||||||
# by integrations. It is only used for internal tracking of
|
# by integrations. It is only used for internal tracking of
|
||||||
@ -101,6 +102,7 @@ from .setup import (
|
|||||||
async_setup_component,
|
async_setup_component,
|
||||||
)
|
)
|
||||||
from .util.async_ import create_eager_task
|
from .util.async_ import create_eager_task
|
||||||
|
from .util.hass_dict import HassKey
|
||||||
from .util.logging import async_activate_log_queue_handler
|
from .util.logging import async_activate_log_queue_handler
|
||||||
from .util.package import async_get_user_site, is_virtual_env
|
from .util.package import async_get_user_site, is_virtual_env
|
||||||
|
|
||||||
@ -120,7 +122,7 @@ SETUP_ORDER_SORT_KEY = partial(contains, BASE_PLATFORMS)
|
|||||||
ERROR_LOG_FILENAME = "home-assistant.log"
|
ERROR_LOG_FILENAME = "home-assistant.log"
|
||||||
|
|
||||||
# hass.data key for logging information.
|
# hass.data key for logging information.
|
||||||
DATA_REGISTRIES_LOADED = "bootstrap_registries_loaded"
|
DATA_REGISTRIES_LOADED: HassKey[None] = HassKey("bootstrap_registries_loaded")
|
||||||
|
|
||||||
LOG_SLOW_STARTUP_INTERVAL = 60
|
LOG_SLOW_STARTUP_INTERVAL = 60
|
||||||
SLOW_STARTUP_CHECK_INTERVAL = 1
|
SLOW_STARTUP_CHECK_INTERVAL = 1
|
||||||
@ -132,8 +134,15 @@ COOLDOWN_TIME = 60
|
|||||||
|
|
||||||
|
|
||||||
DEBUGGER_INTEGRATIONS = {"debugpy"}
|
DEBUGGER_INTEGRATIONS = {"debugpy"}
|
||||||
|
|
||||||
|
# Core integrations are unconditionally loaded
|
||||||
CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"}
|
CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"}
|
||||||
LOGGING_INTEGRATIONS = {
|
|
||||||
|
# Integrations that are loaded right after the core is set up
|
||||||
|
LOGGING_AND_HTTP_DEPS_INTEGRATIONS = {
|
||||||
|
# isal is loaded right away before `http` to ensure if its
|
||||||
|
# enabled, that `isal` is up to date.
|
||||||
|
"isal",
|
||||||
# Set log levels
|
# Set log levels
|
||||||
"logger",
|
"logger",
|
||||||
# Error logging
|
# Error logging
|
||||||
@ -212,8 +221,8 @@ CRITICAL_INTEGRATIONS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SETUP_ORDER = (
|
SETUP_ORDER = (
|
||||||
# Load logging as soon as possible
|
# Load logging and http deps as soon as possible
|
||||||
("logging", LOGGING_INTEGRATIONS),
|
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
|
||||||
# Setup frontend and recorder
|
# Setup frontend and recorder
|
||||||
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}),
|
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}),
|
||||||
# Start up debuggers. Start these first in case they want to wait.
|
# Start up debuggers. Start these first in case they want to wait.
|
||||||
@ -247,22 +256,39 @@ async def async_setup_hass(
|
|||||||
runtime_config: RuntimeConfig,
|
runtime_config: RuntimeConfig,
|
||||||
) -> core.HomeAssistant | None:
|
) -> core.HomeAssistant | None:
|
||||||
"""Set up Home Assistant."""
|
"""Set up Home Assistant."""
|
||||||
hass = core.HomeAssistant(runtime_config.config_dir)
|
|
||||||
|
|
||||||
async_enable_logging(
|
def create_hass() -> core.HomeAssistant:
|
||||||
hass,
|
"""Create the hass object and do basic setup."""
|
||||||
runtime_config.verbose,
|
hass = core.HomeAssistant(runtime_config.config_dir)
|
||||||
runtime_config.log_rotate_days,
|
loader.async_setup(hass)
|
||||||
runtime_config.log_file,
|
|
||||||
runtime_config.log_no_color,
|
|
||||||
)
|
|
||||||
|
|
||||||
if runtime_config.debug or hass.loop.get_debug():
|
async_enable_logging(
|
||||||
hass.config.debug = True
|
hass,
|
||||||
|
runtime_config.verbose,
|
||||||
|
runtime_config.log_rotate_days,
|
||||||
|
runtime_config.log_file,
|
||||||
|
runtime_config.log_no_color,
|
||||||
|
)
|
||||||
|
|
||||||
|
if runtime_config.debug or hass.loop.get_debug():
|
||||||
|
hass.config.debug = True
|
||||||
|
|
||||||
|
hass.config.safe_mode = runtime_config.safe_mode
|
||||||
|
hass.config.skip_pip = runtime_config.skip_pip
|
||||||
|
hass.config.skip_pip_packages = runtime_config.skip_pip_packages
|
||||||
|
|
||||||
|
return hass
|
||||||
|
|
||||||
|
async def stop_hass(hass: core.HomeAssistant) -> None:
|
||||||
|
"""Stop hass."""
|
||||||
|
# Ask integrations to shut down. It's messy but we can't
|
||||||
|
# do a clean stop without knowing what is broken
|
||||||
|
with contextlib.suppress(TimeoutError):
|
||||||
|
async with hass.timeout.async_timeout(10):
|
||||||
|
await hass.async_stop()
|
||||||
|
|
||||||
|
hass = create_hass()
|
||||||
|
|
||||||
hass.config.safe_mode = runtime_config.safe_mode
|
|
||||||
hass.config.skip_pip = runtime_config.skip_pip
|
|
||||||
hass.config.skip_pip_packages = runtime_config.skip_pip_packages
|
|
||||||
if runtime_config.skip_pip or runtime_config.skip_pip_packages:
|
if runtime_config.skip_pip or runtime_config.skip_pip_packages:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Skipping pip installation of required modules. This may cause issues"
|
"Skipping pip installation of required modules. This may cause issues"
|
||||||
@ -274,7 +300,6 @@ async def async_setup_hass(
|
|||||||
|
|
||||||
_LOGGER.info("Config directory: %s", runtime_config.config_dir)
|
_LOGGER.info("Config directory: %s", runtime_config.config_dir)
|
||||||
|
|
||||||
loader.async_setup(hass)
|
|
||||||
block_async_io.enable()
|
block_async_io.enable()
|
||||||
|
|
||||||
config_dict = None
|
config_dict = None
|
||||||
@ -300,27 +325,28 @@ async def async_setup_hass(
|
|||||||
|
|
||||||
if config_dict is None:
|
if config_dict is None:
|
||||||
recovery_mode = True
|
recovery_mode = True
|
||||||
|
await stop_hass(hass)
|
||||||
|
hass = create_hass()
|
||||||
|
|
||||||
elif not basic_setup_success:
|
elif not basic_setup_success:
|
||||||
_LOGGER.warning("Unable to set up core integrations. Activating recovery mode")
|
_LOGGER.warning("Unable to set up core integrations. Activating recovery mode")
|
||||||
recovery_mode = True
|
recovery_mode = True
|
||||||
|
await stop_hass(hass)
|
||||||
|
hass = create_hass()
|
||||||
|
|
||||||
elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS):
|
elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS):
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Detected that %s did not load. Activating recovery mode",
|
"Detected that %s did not load. Activating recovery mode",
|
||||||
",".join(CRITICAL_INTEGRATIONS),
|
",".join(CRITICAL_INTEGRATIONS),
|
||||||
)
|
)
|
||||||
# Ask integrations to shut down. It's messy but we can't
|
|
||||||
# do a clean stop without knowing what is broken
|
|
||||||
with contextlib.suppress(TimeoutError):
|
|
||||||
async with hass.timeout.async_timeout(10):
|
|
||||||
await hass.async_stop()
|
|
||||||
|
|
||||||
recovery_mode = True
|
|
||||||
old_config = hass.config
|
old_config = hass.config
|
||||||
old_logging = hass.data.get(DATA_LOGGING)
|
old_logging = hass.data.get(DATA_LOGGING)
|
||||||
|
|
||||||
hass = core.HomeAssistant(old_config.config_dir)
|
recovery_mode = True
|
||||||
|
await stop_hass(hass)
|
||||||
|
hass = create_hass()
|
||||||
|
|
||||||
if old_logging:
|
if old_logging:
|
||||||
hass.data[DATA_LOGGING] = old_logging
|
hass.data[DATA_LOGGING] = old_logging
|
||||||
hass.config.debug = old_config.debug
|
hass.config.debug = old_config.debug
|
||||||
@ -370,23 +396,24 @@ def open_hass_ui(hass: core.HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _init_blocking_io_modules_in_executor() -> None:
|
||||||
|
"""Initialize modules that do blocking I/O in executor."""
|
||||||
|
# Cache the result of platform.uname().processor in the executor.
|
||||||
|
# Multiple modules call this function at startup which
|
||||||
|
# executes a blocking subprocess call. This is a problem for the
|
||||||
|
# asyncio event loop. By priming the cache of uname we can
|
||||||
|
# avoid the blocking call in the event loop.
|
||||||
|
_ = platform.uname().processor
|
||||||
|
# Initialize the mimetypes module to avoid blocking calls
|
||||||
|
# to the filesystem to load the mime.types file.
|
||||||
|
mimetypes.init()
|
||||||
|
|
||||||
|
|
||||||
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||||
"""Load the registries and cache the result of platform.uname().processor."""
|
"""Load the registries and modules that will do blocking I/O."""
|
||||||
if DATA_REGISTRIES_LOADED in hass.data:
|
if DATA_REGISTRIES_LOADED in hass.data:
|
||||||
return
|
return
|
||||||
hass.data[DATA_REGISTRIES_LOADED] = None
|
hass.data[DATA_REGISTRIES_LOADED] = None
|
||||||
|
|
||||||
def _cache_uname_processor() -> None:
|
|
||||||
"""Cache the result of platform.uname().processor in the executor.
|
|
||||||
|
|
||||||
Multiple modules call this function at startup which
|
|
||||||
executes a blocking subprocess call. This is a problem for the
|
|
||||||
asyncio event loop. By primeing the cache of uname we can
|
|
||||||
avoid the blocking call in the event loop.
|
|
||||||
"""
|
|
||||||
_ = platform.uname().processor
|
|
||||||
|
|
||||||
# Load the registries and cache the result of platform.uname().processor
|
|
||||||
translation.async_setup(hass)
|
translation.async_setup(hass)
|
||||||
entity.async_setup(hass)
|
entity.async_setup(hass)
|
||||||
template.async_setup(hass)
|
template.async_setup(hass)
|
||||||
@ -399,7 +426,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
|||||||
create_eager_task(floor_registry.async_load(hass)),
|
create_eager_task(floor_registry.async_load(hass)),
|
||||||
create_eager_task(issue_registry.async_load(hass)),
|
create_eager_task(issue_registry.async_load(hass)),
|
||||||
create_eager_task(label_registry.async_load(hass)),
|
create_eager_task(label_registry.async_load(hass)),
|
||||||
hass.async_add_executor_job(_cache_uname_processor),
|
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
|
||||||
create_eager_task(template.async_load_custom_templates(hass)),
|
create_eager_task(template.async_load_custom_templates(hass)),
|
||||||
create_eager_task(restore_state.async_load(hass)),
|
create_eager_task(restore_state.async_load(hass)),
|
||||||
create_eager_task(hass.config_entries.async_initialize()),
|
create_eager_task(hass.config_entries.async_initialize()),
|
||||||
@ -418,6 +445,9 @@ async def async_from_config_dict(
|
|||||||
start = monotonic()
|
start = monotonic()
|
||||||
|
|
||||||
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
||||||
|
# Prime custom component cache early so we know if registry entries are tied
|
||||||
|
# to a custom integration
|
||||||
|
await loader.async_get_custom_components(hass)
|
||||||
await async_load_base_functionality(hass)
|
await async_load_base_functionality(hass)
|
||||||
|
|
||||||
# Set up core.
|
# Set up core.
|
||||||
@ -426,7 +456,11 @@ async def async_from_config_dict(
|
|||||||
if not all(
|
if not all(
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*(
|
*(
|
||||||
create_eager_task(async_setup_component(hass, domain, config))
|
create_eager_task(
|
||||||
|
async_setup_component(hass, domain, config),
|
||||||
|
name=f"bootstrap setup {domain}",
|
||||||
|
loop=hass.loop,
|
||||||
|
)
|
||||||
for domain in CORE_INTEGRATIONS
|
for domain in CORE_INTEGRATIONS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -680,7 +714,7 @@ class _WatchPendingSetups:
|
|||||||
|
|
||||||
if remaining_with_setup_started:
|
if remaining_with_setup_started:
|
||||||
_LOGGER.debug("Integration remaining: %s", remaining_with_setup_started)
|
_LOGGER.debug("Integration remaining: %s", remaining_with_setup_started)
|
||||||
elif waiting_tasks := self._hass._active_tasks: # pylint: disable=protected-access
|
elif waiting_tasks := self._hass._active_tasks: # noqa: SLF001
|
||||||
_LOGGER.debug("Waiting on tasks: %s", waiting_tasks)
|
_LOGGER.debug("Waiting on tasks: %s", waiting_tasks)
|
||||||
self._async_dispatch(remaining_with_setup_started)
|
self._async_dispatch(remaining_with_setup_started)
|
||||||
if (
|
if (
|
||||||
@ -700,7 +734,7 @@ class _WatchPendingSetups:
|
|||||||
def _async_dispatch(self, remaining_with_setup_started: dict[str, float]) -> None:
|
def _async_dispatch(self, remaining_with_setup_started: dict[str, float]) -> None:
|
||||||
"""Dispatch the signal."""
|
"""Dispatch the signal."""
|
||||||
if remaining_with_setup_started or not self._previous_was_empty:
|
if remaining_with_setup_started or not self._previous_was_empty:
|
||||||
async_dispatcher_send(
|
async_dispatcher_send_internal(
|
||||||
self._hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, remaining_with_setup_started
|
self._hass, SIGNAL_BOOTSTRAP_INTEGRATIONS, remaining_with_setup_started
|
||||||
)
|
)
|
||||||
self._previous_was_empty = not remaining_with_setup_started
|
self._previous_was_empty = not remaining_with_setup_started
|
||||||
@ -984,7 +1018,7 @@ async def _async_set_up_integrations(
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Setup timed out for stage 1 waiting on %s - moving forward",
|
"Setup timed out for stage 1 waiting on %s - moving forward",
|
||||||
hass._active_tasks, # pylint: disable=protected-access
|
hass._active_tasks, # noqa: SLF001
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add after dependencies when setting up stage 2 domains
|
# Add after dependencies when setting up stage 2 domains
|
||||||
@ -1000,7 +1034,7 @@ async def _async_set_up_integrations(
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Setup timed out for stage 2 waiting on %s - moving forward",
|
"Setup timed out for stage 2 waiting on %s - moving forward",
|
||||||
hass._active_tasks, # pylint: disable=protected-access
|
hass._active_tasks, # noqa: SLF001
|
||||||
)
|
)
|
||||||
|
|
||||||
# Wrap up startup
|
# Wrap up startup
|
||||||
@ -1011,7 +1045,7 @@ async def _async_set_up_integrations(
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Setup timed out for bootstrap waiting on %s - moving forward",
|
"Setup timed out for bootstrap waiting on %s - moving forward",
|
||||||
hass._active_tasks, # pylint: disable=protected-access
|
hass._active_tasks, # noqa: SLF001
|
||||||
)
|
)
|
||||||
|
|
||||||
watcher.async_stop()
|
watcher.async_stop()
|
||||||
|
5
homeassistant/brands/ambient_weather.json
Normal file
5
homeassistant/brands/ambient_weather.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "ambient_weather",
|
||||||
|
"name": "Ambient Weather",
|
||||||
|
"integrations": ["ambient_network", "ambient_station"]
|
||||||
|
}
|
5
homeassistant/brands/ruuvi.json
Normal file
5
homeassistant/brands/ruuvi.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "ruuvi",
|
||||||
|
"name": "Ruuvi",
|
||||||
|
"integrations": ["ruuvi_gateway", "ruuvitag_ble"]
|
||||||
|
}
|
5
homeassistant/brands/weatherflow.json
Normal file
5
homeassistant/brands/weatherflow.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "weatherflow",
|
||||||
|
"name": "WeatherFlow",
|
||||||
|
"integrations": ["weatherflow", "weatherflow_cloud"]
|
||||||
|
}
|
@ -5,9 +5,7 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from jaraco.abode.automation import Automation as AbodeAuto
|
|
||||||
from jaraco.abode.client import Client as Abode
|
from jaraco.abode.client import Client as Abode
|
||||||
from jaraco.abode.devices.base import Device as AbodeDev
|
|
||||||
from jaraco.abode.exceptions import (
|
from jaraco.abode.exceptions import (
|
||||||
AuthenticationException as AbodeAuthenticationException,
|
AuthenticationException as AbodeAuthenticationException,
|
||||||
Exception as AbodeException,
|
Exception as AbodeException,
|
||||||
@ -29,11 +27,11 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, ServiceCall
|
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, ServiceCall
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers import config_validation as cv, entity
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER
|
from .const import CONF_POLLING, DOMAIN, LOGGER
|
||||||
|
|
||||||
SERVICE_SETTINGS = "change_setting"
|
SERVICE_SETTINGS = "change_setting"
|
||||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||||
@ -83,6 +81,12 @@ class AbodeSystem:
|
|||||||
logout_listener: CALLBACK_TYPE | None = None
|
logout_listener: CALLBACK_TYPE | None = None
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up the Abode component."""
|
||||||
|
setup_hass_services(hass)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Abode integration from a config entry."""
|
"""Set up Abode integration from a config entry."""
|
||||||
username = entry.data[CONF_USERNAME]
|
username = entry.data[CONF_USERNAME]
|
||||||
@ -111,7 +115,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
await setup_hass_events(hass)
|
await setup_hass_events(hass)
|
||||||
await hass.async_add_executor_job(setup_hass_services, hass)
|
|
||||||
await hass.async_add_executor_job(setup_abode_events, hass)
|
await hass.async_add_executor_job(setup_abode_events, hass)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -119,10 +122,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_SETTINGS)
|
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE)
|
|
||||||
hass.services.async_remove(DOMAIN, SERVICE_TRIGGER_AUTOMATION)
|
|
||||||
|
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop)
|
await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop)
|
||||||
@ -175,15 +174,15 @@ def setup_hass_services(hass: HomeAssistant) -> None:
|
|||||||
signal = f"abode_trigger_automation_{entity_id}"
|
signal = f"abode_trigger_automation_{entity_id}"
|
||||||
dispatcher_send(hass, signal)
|
dispatcher_send(hass, signal)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
|
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA
|
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_TRIGGER_AUTOMATION, trigger_automation, schema=AUTOMATION_SCHEMA
|
DOMAIN, SERVICE_TRIGGER_AUTOMATION, trigger_automation, schema=AUTOMATION_SCHEMA
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -247,108 +246,3 @@ def setup_abode_events(hass: HomeAssistant) -> None:
|
|||||||
hass.data[DOMAIN].abode.events.add_event_callback(
|
hass.data[DOMAIN].abode.events.add_event_callback(
|
||||||
event, partial(event_callback, event)
|
event, partial(event_callback, event)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AbodeEntity(entity.Entity):
|
|
||||||
"""Representation of an Abode entity."""
|
|
||||||
|
|
||||||
_attr_attribution = ATTRIBUTION
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(self, data: AbodeSystem) -> None:
|
|
||||||
"""Initialize Abode entity."""
|
|
||||||
self._data = data
|
|
||||||
self._attr_should_poll = data.polling
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Subscribe to Abode connection status updates."""
|
|
||||||
await self.hass.async_add_executor_job(
|
|
||||||
self._data.abode.events.add_connection_status_callback,
|
|
||||||
self.unique_id,
|
|
||||||
self._update_connection_status,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.hass.data[DOMAIN].entity_ids.add(self.entity_id)
|
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
|
||||||
"""Unsubscribe from Abode connection status updates."""
|
|
||||||
await self.hass.async_add_executor_job(
|
|
||||||
self._data.abode.events.remove_connection_status_callback, self.unique_id
|
|
||||||
)
|
|
||||||
|
|
||||||
def _update_connection_status(self) -> None:
|
|
||||||
"""Update the entity available property."""
|
|
||||||
self._attr_available = self._data.abode.events.connected
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
|
|
||||||
|
|
||||||
class AbodeDevice(AbodeEntity):
|
|
||||||
"""Representation of an Abode device."""
|
|
||||||
|
|
||||||
def __init__(self, data: AbodeSystem, device: AbodeDev) -> None:
|
|
||||||
"""Initialize Abode device."""
|
|
||||||
super().__init__(data)
|
|
||||||
self._device = device
|
|
||||||
self._attr_unique_id = device.uuid
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Subscribe to device events."""
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
await self.hass.async_add_executor_job(
|
|
||||||
self._data.abode.events.add_device_callback,
|
|
||||||
self._device.id,
|
|
||||||
self._update_callback,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
|
||||||
"""Unsubscribe from device events."""
|
|
||||||
await super().async_will_remove_from_hass()
|
|
||||||
await self.hass.async_add_executor_job(
|
|
||||||
self._data.abode.events.remove_all_device_callbacks, self._device.id
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(self) -> None:
|
|
||||||
"""Update device state."""
|
|
||||||
self._device.refresh()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def extra_state_attributes(self) -> dict[str, str]:
|
|
||||||
"""Return the state attributes."""
|
|
||||||
return {
|
|
||||||
"device_id": self._device.id,
|
|
||||||
"battery_low": self._device.battery_low,
|
|
||||||
"no_response": self._device.no_response,
|
|
||||||
"device_type": self._device.type,
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self) -> DeviceInfo:
|
|
||||||
"""Return device registry information for this entity."""
|
|
||||||
return DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, self._device.id)},
|
|
||||||
manufacturer="Abode",
|
|
||||||
model=self._device.type,
|
|
||||||
name=self._device.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _update_callback(self, device: AbodeDev) -> None:
|
|
||||||
"""Update the device state."""
|
|
||||||
self.schedule_update_ha_state()
|
|
||||||
|
|
||||||
|
|
||||||
class AbodeAutomation(AbodeEntity):
|
|
||||||
"""Representation of an Abode automation."""
|
|
||||||
|
|
||||||
def __init__(self, data: AbodeSystem, automation: AbodeAuto) -> None:
|
|
||||||
"""Initialize for Abode automation."""
|
|
||||||
super().__init__(data)
|
|
||||||
self._automation = automation
|
|
||||||
self._attr_name = automation.name
|
|
||||||
self._attr_unique_id = automation.automation_id
|
|
||||||
self._attr_extra_state_attributes = {
|
|
||||||
"type": "CUE automation",
|
|
||||||
}
|
|
||||||
|
|
||||||
def update(self) -> None:
|
|
||||||
"""Update automation state."""
|
|
||||||
self._automation.refresh()
|
|
||||||
|
@ -17,8 +17,9 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AbodeDevice, AbodeSystem
|
from . import AbodeSystem
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .entity import AbodeDevice
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@ -22,8 +22,9 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util.enum import try_parse_enum
|
from homeassistant.util.enum import try_parse_enum
|
||||||
|
|
||||||
from . import AbodeDevice, AbodeSystem
|
from . import AbodeSystem
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .entity import AbodeDevice
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@ -19,8 +19,9 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
from . import AbodeDevice, AbodeSystem
|
from . import AbodeSystem
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
|
from .entity import AbodeDevice
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||||
|
|
||||||
|
@ -10,8 +10,9 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AbodeDevice, AbodeSystem
|
from . import AbodeSystem
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .entity import AbodeDevice
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
115
homeassistant/components/abode/entity.py
Normal file
115
homeassistant/components/abode/entity.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
"""Support for Abode Security System entities."""
|
||||||
|
|
||||||
|
from jaraco.abode.automation import Automation as AbodeAuto
|
||||||
|
from jaraco.abode.devices.base import Device as AbodeDev
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from . import AbodeSystem
|
||||||
|
from .const import ATTRIBUTION, DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class AbodeEntity(Entity):
|
||||||
|
"""Representation of an Abode entity."""
|
||||||
|
|
||||||
|
_attr_attribution = ATTRIBUTION
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(self, data: AbodeSystem) -> None:
|
||||||
|
"""Initialize Abode entity."""
|
||||||
|
self._data = data
|
||||||
|
self._attr_should_poll = data.polling
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Subscribe to Abode connection status updates."""
|
||||||
|
await self.hass.async_add_executor_job(
|
||||||
|
self._data.abode.events.add_connection_status_callback,
|
||||||
|
self.unique_id,
|
||||||
|
self._update_connection_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.hass.data[DOMAIN].entity_ids.add(self.entity_id)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Unsubscribe from Abode connection status updates."""
|
||||||
|
await self.hass.async_add_executor_job(
|
||||||
|
self._data.abode.events.remove_connection_status_callback, self.unique_id
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_connection_status(self) -> None:
|
||||||
|
"""Update the entity available property."""
|
||||||
|
self._attr_available = self._data.abode.events.connected
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class AbodeDevice(AbodeEntity):
|
||||||
|
"""Representation of an Abode device."""
|
||||||
|
|
||||||
|
def __init__(self, data: AbodeSystem, device: AbodeDev) -> None:
|
||||||
|
"""Initialize Abode device."""
|
||||||
|
super().__init__(data)
|
||||||
|
self._device = device
|
||||||
|
self._attr_unique_id = device.uuid
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Subscribe to device events."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
await self.hass.async_add_executor_job(
|
||||||
|
self._data.abode.events.add_device_callback,
|
||||||
|
self._device.id,
|
||||||
|
self._update_callback,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Unsubscribe from device events."""
|
||||||
|
await super().async_will_remove_from_hass()
|
||||||
|
await self.hass.async_add_executor_job(
|
||||||
|
self._data.abode.events.remove_all_device_callbacks, self._device.id
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
"""Update device state."""
|
||||||
|
self._device.refresh()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self) -> dict[str, str]:
|
||||||
|
"""Return the state attributes."""
|
||||||
|
return {
|
||||||
|
"device_id": self._device.id,
|
||||||
|
"battery_low": self._device.battery_low,
|
||||||
|
"no_response": self._device.no_response,
|
||||||
|
"device_type": self._device.type,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return device registry information for this entity."""
|
||||||
|
return DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, self._device.id)},
|
||||||
|
manufacturer="Abode",
|
||||||
|
model=self._device.type,
|
||||||
|
name=self._device.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_callback(self, device: AbodeDev) -> None:
|
||||||
|
"""Update the device state."""
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class AbodeAutomation(AbodeEntity):
|
||||||
|
"""Representation of an Abode automation."""
|
||||||
|
|
||||||
|
def __init__(self, data: AbodeSystem, automation: AbodeAuto) -> None:
|
||||||
|
"""Initialize for Abode automation."""
|
||||||
|
super().__init__(data)
|
||||||
|
self._automation = automation
|
||||||
|
self._attr_name = automation.name
|
||||||
|
self._attr_unique_id = automation.automation_id
|
||||||
|
self._attr_extra_state_attributes = {
|
||||||
|
"type": "CUE automation",
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
"""Update automation state."""
|
||||||
|
self._automation.refresh()
|
@ -23,8 +23,9 @@ from homeassistant.util.color import (
|
|||||||
color_temperature_mired_to_kelvin,
|
color_temperature_mired_to_kelvin,
|
||||||
)
|
)
|
||||||
|
|
||||||
from . import AbodeDevice, AbodeSystem
|
from . import AbodeSystem
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .entity import AbodeDevice
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@ -10,8 +10,9 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AbodeDevice, AbodeSystem
|
from . import AbodeSystem
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .entity import AbodeDevice
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@ -27,8 +27,9 @@ from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AbodeDevice, AbodeSystem
|
from . import AbodeSystem
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .entity import AbodeDevice
|
||||||
|
|
||||||
ABODE_TEMPERATURE_UNIT_HA_UNIT = {
|
ABODE_TEMPERATURE_UNIT_HA_UNIT = {
|
||||||
UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,
|
UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,
|
||||||
|
@ -13,8 +13,9 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import AbodeAutomation, AbodeDevice, AbodeSystem
|
from . import AbodeSystem
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .entity import AbodeAutomation, AbodeDevice
|
||||||
|
|
||||||
DEVICE_TYPES = [TYPE_SWITCH, TYPE_VALVE]
|
DEVICE_TYPES = [TYPE_SWITCH, TYPE_VALVE]
|
||||||
|
|
||||||
|
@ -33,7 +33,10 @@ class AccuWeatherData:
|
|||||||
coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
|
coordinator_daily_forecast: AccuWeatherDailyForecastDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
type AccuWeatherConfigEntry = ConfigEntry[AccuWeatherData]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: AccuWeatherConfigEntry) -> bool:
|
||||||
"""Set up AccuWeather as config entry."""
|
"""Set up AccuWeather as config entry."""
|
||||||
api_key: str = entry.data[CONF_API_KEY]
|
api_key: str = entry.data[CONF_API_KEY]
|
||||||
name: str = entry.data[CONF_NAME]
|
name: str = entry.data[CONF_NAME]
|
||||||
@ -64,9 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
await coordinator_observation.async_config_entry_first_refresh()
|
await coordinator_observation.async_config_entry_first_refresh()
|
||||||
await coordinator_daily_forecast.async_config_entry_first_refresh()
|
await coordinator_daily_forecast.async_config_entry_first_refresh()
|
||||||
|
|
||||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
entry.runtime_data = AccuWeatherData(
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AccuWeatherData(
|
|
||||||
coordinator_observation=coordinator_observation,
|
coordinator_observation=coordinator_observation,
|
||||||
coordinator_daily_forecast=coordinator_daily_forecast,
|
coordinator_daily_forecast=coordinator_daily_forecast,
|
||||||
)
|
)
|
||||||
@ -84,16 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: AccuWeatherConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
|
||||||
|
|
||||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
||||||
"""Update listener."""
|
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
|
||||||
|
@ -5,21 +5,19 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import AccuWeatherData
|
from . import AccuWeatherConfigEntry, AccuWeatherData
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE}
|
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE}
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, config_entry: ConfigEntry
|
hass: HomeAssistant, config_entry: AccuWeatherConfigEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
accuweather_data: AccuWeatherData = hass.data[DOMAIN][config_entry.entry_id]
|
accuweather_data: AccuWeatherData = config_entry.runtime_data
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT),
|
"config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT),
|
||||||
|
51
homeassistant/components/accuweather/icons.json
Normal file
51
homeassistant/components/accuweather/icons.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"cloud_ceiling": {
|
||||||
|
"default": "mdi:weather-fog"
|
||||||
|
},
|
||||||
|
"cloud_cover": {
|
||||||
|
"default": "mdi:weather-cloudy"
|
||||||
|
},
|
||||||
|
"cloud_cover_day": {
|
||||||
|
"default": "mdi:weather-cloudy"
|
||||||
|
},
|
||||||
|
"cloud_cover_night": {
|
||||||
|
"default": "mdi:weather-cloudy"
|
||||||
|
},
|
||||||
|
"grass_pollen": {
|
||||||
|
"default": "mdi:grass"
|
||||||
|
},
|
||||||
|
"hours_of_sun": {
|
||||||
|
"default": "mdi:weather-partly-cloudy"
|
||||||
|
},
|
||||||
|
"mold_pollen": {
|
||||||
|
"default": "mdi:blur"
|
||||||
|
},
|
||||||
|
"pressure_tendency": {
|
||||||
|
"default": "mdi:gauge"
|
||||||
|
},
|
||||||
|
"ragweed_pollen": {
|
||||||
|
"default": "mdi:sprout"
|
||||||
|
},
|
||||||
|
"thunderstorm_probability_day": {
|
||||||
|
"default": "mdi:weather-lightning"
|
||||||
|
},
|
||||||
|
"thunderstorm_probability_night": {
|
||||||
|
"default": "mdi:weather-lightning"
|
||||||
|
},
|
||||||
|
"translation_key": {
|
||||||
|
"default": "mdi:air-filter"
|
||||||
|
},
|
||||||
|
"tree_pollen": {
|
||||||
|
"default": "mdi:tree-outline"
|
||||||
|
},
|
||||||
|
"uv_index": {
|
||||||
|
"default": "mdi:weather-sunny"
|
||||||
|
},
|
||||||
|
"uv_index_forecast": {
|
||||||
|
"default": "mdi:weather-sunny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONCENTRATION_PARTS_PER_CUBIC_METER,
|
CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
@ -28,7 +27,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import AccuWeatherData
|
from . import AccuWeatherConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
API_METRIC,
|
API_METRIC,
|
||||||
ATTR_CATEGORY,
|
ATTR_CATEGORY,
|
||||||
@ -38,7 +37,6 @@ from .const import (
|
|||||||
ATTR_SPEED,
|
ATTR_SPEED,
|
||||||
ATTR_VALUE,
|
ATTR_VALUE,
|
||||||
ATTRIBUTION,
|
ATTRIBUTION,
|
||||||
DOMAIN,
|
|
||||||
MAX_FORECAST_DAYS,
|
MAX_FORECAST_DAYS,
|
||||||
)
|
)
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
@ -57,284 +55,174 @@ class AccuWeatherSensorDescription(SensorEntityDescription):
|
|||||||
attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {}
|
attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||||
class AccuWeatherForecastSensorDescription(AccuWeatherSensorDescription):
|
AccuWeatherSensorDescription(
|
||||||
"""Class describing AccuWeather sensor entities."""
|
key="AirQuality",
|
||||||
|
value_fn=lambda data: cast(str, data[ATTR_CATEGORY]),
|
||||||
day: int
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
options=["good", "hazardous", "high", "low", "moderate", "unhealthy"],
|
||||||
|
translation_key="air_quality",
|
||||||
FORECAST_SENSOR_TYPES: tuple[AccuWeatherForecastSensorDescription, ...] = (
|
|
||||||
*(
|
|
||||||
AccuWeatherForecastSensorDescription(
|
|
||||||
key="AirQuality",
|
|
||||||
icon="mdi:air-filter",
|
|
||||||
value_fn=lambda data: cast(str, data[ATTR_CATEGORY]),
|
|
||||||
device_class=SensorDeviceClass.ENUM,
|
|
||||||
options=["good", "hazardous", "high", "low", "moderate", "unhealthy"],
|
|
||||||
translation_key=f"air_quality_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="CloudCoverDay",
|
||||||
key="CloudCoverDay",
|
entity_registry_enabled_default=False,
|
||||||
icon="mdi:weather-cloudy",
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
entity_registry_enabled_default=False,
|
value_fn=lambda data: cast(int, data),
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
translation_key="cloud_cover_day",
|
||||||
value_fn=lambda data: cast(int, data),
|
|
||||||
translation_key=f"cloud_cover_day_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="CloudCoverNight",
|
||||||
key="CloudCoverNight",
|
entity_registry_enabled_default=False,
|
||||||
icon="mdi:weather-cloudy",
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
entity_registry_enabled_default=False,
|
value_fn=lambda data: cast(int, data),
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
translation_key="cloud_cover_night",
|
||||||
value_fn=lambda data: cast(int, data),
|
|
||||||
translation_key=f"cloud_cover_night_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="Grass",
|
||||||
key="Grass",
|
entity_registry_enabled_default=False,
|
||||||
icon="mdi:grass",
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
entity_registry_enabled_default=False,
|
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
translation_key="grass_pollen",
|
||||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
|
||||||
translation_key=f"grass_pollen_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="HoursOfSun",
|
||||||
key="HoursOfSun",
|
native_unit_of_measurement=UnitOfTime.HOURS,
|
||||||
icon="mdi:weather-partly-cloudy",
|
value_fn=lambda data: cast(float, data),
|
||||||
native_unit_of_measurement=UnitOfTime.HOURS,
|
translation_key="hours_of_sun",
|
||||||
value_fn=lambda data: cast(float, data),
|
|
||||||
translation_key=f"hours_of_sun_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="LongPhraseDay",
|
||||||
key="LongPhraseDay",
|
value_fn=lambda data: cast(str, data),
|
||||||
value_fn=lambda data: cast(str, data),
|
translation_key="condition_day",
|
||||||
translation_key=f"condition_day_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="LongPhraseNight",
|
||||||
key="LongPhraseNight",
|
value_fn=lambda data: cast(str, data),
|
||||||
value_fn=lambda data: cast(str, data),
|
translation_key="condition_night",
|
||||||
translation_key=f"condition_night_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="Mold",
|
||||||
key="Mold",
|
entity_registry_enabled_default=False,
|
||||||
icon="mdi:blur",
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
entity_registry_enabled_default=False,
|
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
translation_key="mold_pollen",
|
||||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
|
||||||
translation_key=f"mold_pollen_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="Ragweed",
|
||||||
key="Ragweed",
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
icon="mdi:sprout",
|
entity_registry_enabled_default=False,
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||||
entity_registry_enabled_default=False,
|
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
translation_key="ragweed_pollen",
|
||||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
|
||||||
translation_key=f"ragweed_pollen_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="RealFeelTemperatureMax",
|
||||||
key="RealFeelTemperatureMax",
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
translation_key="realfeel_temperature_max",
|
||||||
translation_key=f"realfeel_temperature_max_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="RealFeelTemperatureMin",
|
||||||
key="RealFeelTemperatureMin",
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
translation_key="realfeel_temperature_min",
|
||||||
translation_key=f"realfeel_temperature_min_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="RealFeelTemperatureShadeMax",
|
||||||
key="RealFeelTemperatureShadeMax",
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
entity_registry_enabled_default=False,
|
||||||
entity_registry_enabled_default=False,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
translation_key="realfeel_temperature_shade_max",
|
||||||
translation_key=f"realfeel_temperature_shade_max_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="RealFeelTemperatureShadeMin",
|
||||||
key="RealFeelTemperatureShadeMin",
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
entity_registry_enabled_default=False,
|
||||||
entity_registry_enabled_default=False,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
translation_key="realfeel_temperature_shade_min",
|
||||||
translation_key=f"realfeel_temperature_shade_min_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="SolarIrradianceDay",
|
||||||
key="SolarIrradianceDay",
|
device_class=SensorDeviceClass.IRRADIANCE,
|
||||||
icon="mdi:weather-sunny",
|
entity_registry_enabled_default=False,
|
||||||
entity_registry_enabled_default=False,
|
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
||||||
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
translation_key="solar_irradiance_day",
|
||||||
translation_key=f"solar_irradiance_day_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="SolarIrradianceNight",
|
||||||
key="SolarIrradianceNight",
|
device_class=SensorDeviceClass.IRRADIANCE,
|
||||||
icon="mdi:weather-sunny",
|
entity_registry_enabled_default=False,
|
||||||
entity_registry_enabled_default=False,
|
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
||||||
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
translation_key="solar_irradiance_night",
|
||||||
translation_key=f"solar_irradiance_night_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="ThunderstormProbabilityDay",
|
||||||
key="ThunderstormProbabilityDay",
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:weather-lightning",
|
value_fn=lambda data: cast(int, data),
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
translation_key="thunderstorm_probability_day",
|
||||||
value_fn=lambda data: cast(int, data),
|
|
||||||
translation_key=f"thunderstorm_probability_day_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="ThunderstormProbabilityNight",
|
||||||
key="ThunderstormProbabilityNight",
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:weather-lightning",
|
value_fn=lambda data: cast(int, data),
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
translation_key="thunderstorm_probability_night",
|
||||||
value_fn=lambda data: cast(int, data),
|
|
||||||
translation_key=f"thunderstorm_probability_night_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="Tree",
|
||||||
key="Tree",
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||||
icon="mdi:tree-outline",
|
entity_registry_enabled_default=False,
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
|
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||||
entity_registry_enabled_default=False,
|
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
translation_key="tree_pollen",
|
||||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
|
||||||
translation_key=f"tree_pollen_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="UVIndex",
|
||||||
key="UVIndex",
|
native_unit_of_measurement=UV_INDEX,
|
||||||
icon="mdi:weather-sunny",
|
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
||||||
native_unit_of_measurement=UV_INDEX,
|
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
||||||
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
|
translation_key="uv_index_forecast",
|
||||||
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
|
|
||||||
translation_key=f"uv_index_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="WindGustDay",
|
||||||
key="WindGustDay",
|
device_class=SensorDeviceClass.WIND_SPEED,
|
||||||
device_class=SensorDeviceClass.WIND_SPEED,
|
entity_registry_enabled_default=False,
|
||||||
entity_registry_enabled_default=False,
|
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
||||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
|
||||||
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
|
translation_key="wind_gust_speed_day",
|
||||||
translation_key=f"wind_gust_speed_day_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="WindGustNight",
|
||||||
key="WindGustNight",
|
device_class=SensorDeviceClass.WIND_SPEED,
|
||||||
device_class=SensorDeviceClass.WIND_SPEED,
|
entity_registry_enabled_default=False,
|
||||||
entity_registry_enabled_default=False,
|
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
||||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
|
||||||
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
|
translation_key="wind_gust_speed_night",
|
||||||
translation_key=f"wind_gust_speed_night_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="WindDay",
|
||||||
key="WindDay",
|
device_class=SensorDeviceClass.WIND_SPEED,
|
||||||
device_class=SensorDeviceClass.WIND_SPEED,
|
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
||||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
|
||||||
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
|
translation_key="wind_speed_day",
|
||||||
translation_key=f"wind_speed_day_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
*(
|
AccuWeatherSensorDescription(
|
||||||
AccuWeatherForecastSensorDescription(
|
key="WindNight",
|
||||||
key="WindNight",
|
device_class=SensorDeviceClass.WIND_SPEED,
|
||||||
device_class=SensorDeviceClass.WIND_SPEED,
|
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
||||||
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
|
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
|
||||||
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
|
translation_key="wind_speed_night",
|
||||||
translation_key=f"wind_speed_night_{day}d",
|
|
||||||
day=day,
|
|
||||||
)
|
|
||||||
for day in range(MAX_FORECAST_DAYS + 1)
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -351,7 +239,6 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
|||||||
AccuWeatherSensorDescription(
|
AccuWeatherSensorDescription(
|
||||||
key="Ceiling",
|
key="Ceiling",
|
||||||
device_class=SensorDeviceClass.DISTANCE,
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
icon="mdi:weather-fog",
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=UnitOfLength.METERS,
|
native_unit_of_measurement=UnitOfLength.METERS,
|
||||||
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
|
||||||
@ -360,7 +247,6 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
|||||||
),
|
),
|
||||||
AccuWeatherSensorDescription(
|
AccuWeatherSensorDescription(
|
||||||
key="CloudCover",
|
key="CloudCover",
|
||||||
icon="mdi:weather-cloudy",
|
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
@ -405,14 +291,12 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
|||||||
AccuWeatherSensorDescription(
|
AccuWeatherSensorDescription(
|
||||||
key="PressureTendency",
|
key="PressureTendency",
|
||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
icon="mdi:gauge",
|
|
||||||
options=["falling", "rising", "steady"],
|
options=["falling", "rising", "steady"],
|
||||||
value_fn=lambda data: cast(str, data["LocalizedText"]).lower(),
|
value_fn=lambda data: cast(str, data["LocalizedText"]).lower(),
|
||||||
translation_key="pressure_tendency",
|
translation_key="pressure_tendency",
|
||||||
),
|
),
|
||||||
AccuWeatherSensorDescription(
|
AccuWeatherSensorDescription(
|
||||||
key="UVIndex",
|
key="UVIndex",
|
||||||
icon="mdi:weather-sunny",
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=UV_INDEX,
|
native_unit_of_measurement=UV_INDEX,
|
||||||
value_fn=lambda data: cast(int, data),
|
value_fn=lambda data: cast(int, data),
|
||||||
@ -458,17 +342,16 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: AccuWeatherConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add AccuWeather entities from a config_entry."""
|
"""Add AccuWeather entities from a config_entry."""
|
||||||
|
|
||||||
accuweather_data: AccuWeatherData = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
|
|
||||||
observation_coordinator: AccuWeatherObservationDataUpdateCoordinator = (
|
observation_coordinator: AccuWeatherObservationDataUpdateCoordinator = (
|
||||||
accuweather_data.coordinator_observation
|
entry.runtime_data.coordinator_observation
|
||||||
)
|
)
|
||||||
forecast_daily_coordinator: AccuWeatherDailyForecastDataUpdateCoordinator = (
|
forecast_daily_coordinator: AccuWeatherDailyForecastDataUpdateCoordinator = (
|
||||||
accuweather_data.coordinator_daily_forecast
|
entry.runtime_data.coordinator_daily_forecast
|
||||||
)
|
)
|
||||||
|
|
||||||
sensors: list[AccuWeatherSensor | AccuWeatherForecastSensor] = [
|
sensors: list[AccuWeatherSensor | AccuWeatherForecastSensor] = [
|
||||||
@ -478,9 +361,10 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
sensors.extend(
|
sensors.extend(
|
||||||
[
|
[
|
||||||
AccuWeatherForecastSensor(forecast_daily_coordinator, description)
|
AccuWeatherForecastSensor(forecast_daily_coordinator, description, day)
|
||||||
|
for day in range(MAX_FORECAST_DAYS + 1)
|
||||||
for description in FORECAST_SENSOR_TYPES
|
for description in FORECAST_SENSOR_TYPES
|
||||||
if description.key in forecast_daily_coordinator.data[description.day]
|
if description.key in forecast_daily_coordinator.data[day]
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -546,25 +430,27 @@ class AccuWeatherForecastSensor(
|
|||||||
|
|
||||||
_attr_attribution = ATTRIBUTION
|
_attr_attribution = ATTRIBUTION
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
entity_description: AccuWeatherForecastSensorDescription
|
entity_description: AccuWeatherSensorDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: AccuWeatherDailyForecastDataUpdateCoordinator,
|
coordinator: AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||||
description: AccuWeatherForecastSensorDescription,
|
description: AccuWeatherSensorDescription,
|
||||||
|
forecast_day: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self.forecast_day = description.day
|
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._sensor_data = self._get_sensor_data(
|
self._sensor_data = self._get_sensor_data(
|
||||||
coordinator.data, description.key, self.forecast_day
|
coordinator.data, description.key, forecast_day
|
||||||
)
|
)
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = (
|
||||||
f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower()
|
f"{coordinator.location_key}-{description.key}-{forecast_day}".lower()
|
||||||
)
|
)
|
||||||
self._attr_device_info = coordinator.device_info
|
self._attr_device_info = coordinator.device_info
|
||||||
|
self._attr_translation_placeholders = {"forecast_day": str(forecast_day)}
|
||||||
|
self.forecast_day = forecast_day
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> str | int | float | None:
|
def native_value(self) -> str | int | float | None:
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"air_quality_0d": {
|
"air_quality": {
|
||||||
"name": "Air quality today",
|
"name": "Air quality day {forecast_day}",
|
||||||
"state": {
|
"state": {
|
||||||
"good": "Good",
|
"good": "Good",
|
||||||
"hazardous": "Hazardous",
|
"hazardous": "Hazardous",
|
||||||
@ -32,50 +32,6 @@
|
|||||||
"unhealthy": "Unhealthy"
|
"unhealthy": "Unhealthy"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"air_quality_1d": {
|
|
||||||
"name": "Air quality day 1",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"air_quality_2d": {
|
|
||||||
"name": "Air quality day 2",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"air_quality_3d": {
|
|
||||||
"name": "Air quality day 3",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"air_quality_4d": {
|
|
||||||
"name": "Air quality day 4",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apparent_temperature": {
|
"apparent_temperature": {
|
||||||
"name": "Apparent temperature"
|
"name": "Apparent temperature"
|
||||||
},
|
},
|
||||||
@ -85,240 +41,52 @@
|
|||||||
"cloud_cover": {
|
"cloud_cover": {
|
||||||
"name": "Cloud cover"
|
"name": "Cloud cover"
|
||||||
},
|
},
|
||||||
"cloud_cover_day_0d": {
|
"cloud_cover_day": {
|
||||||
"name": "Cloud cover today"
|
"name": "Cloud cover day {forecast_day}"
|
||||||
},
|
},
|
||||||
"cloud_cover_day_1d": {
|
"cloud_cover_night": {
|
||||||
"name": "Cloud cover day 1"
|
"name": "Cloud cover night {forecast_day}"
|
||||||
},
|
},
|
||||||
"cloud_cover_day_2d": {
|
"condition_day": {
|
||||||
"name": "Cloud cover day 2"
|
"name": "Condition day {forecast_day}"
|
||||||
},
|
},
|
||||||
"cloud_cover_day_3d": {
|
"condition_night": {
|
||||||
"name": "Cloud cover day 3"
|
"name": "Condition night {forecast_day}"
|
||||||
},
|
|
||||||
"cloud_cover_day_4d": {
|
|
||||||
"name": "Cloud cover day 4"
|
|
||||||
},
|
|
||||||
"cloud_cover_night_0d": {
|
|
||||||
"name": "Cloud cover tonight"
|
|
||||||
},
|
|
||||||
"cloud_cover_night_1d": {
|
|
||||||
"name": "Cloud cover night 1"
|
|
||||||
},
|
|
||||||
"cloud_cover_night_2d": {
|
|
||||||
"name": "Cloud cover night 2"
|
|
||||||
},
|
|
||||||
"cloud_cover_night_3d": {
|
|
||||||
"name": "Cloud cover night 3"
|
|
||||||
},
|
|
||||||
"cloud_cover_night_4d": {
|
|
||||||
"name": "Cloud cover night 4"
|
|
||||||
},
|
|
||||||
"condition_day_0d": {
|
|
||||||
"name": "Condition today"
|
|
||||||
},
|
|
||||||
"condition_day_1d": {
|
|
||||||
"name": "Condition day 1"
|
|
||||||
},
|
|
||||||
"condition_day_2d": {
|
|
||||||
"name": "Condition day 2"
|
|
||||||
},
|
|
||||||
"condition_day_3d": {
|
|
||||||
"name": "Condition day 3"
|
|
||||||
},
|
|
||||||
"condition_day_4d": {
|
|
||||||
"name": "Condition day 4"
|
|
||||||
},
|
|
||||||
"condition_night_0d": {
|
|
||||||
"name": "Condition tonight"
|
|
||||||
},
|
|
||||||
"condition_night_1d": {
|
|
||||||
"name": "Condition night 1"
|
|
||||||
},
|
|
||||||
"condition_night_2d": {
|
|
||||||
"name": "Condition night 2"
|
|
||||||
},
|
|
||||||
"condition_night_3d": {
|
|
||||||
"name": "Condition night 3"
|
|
||||||
},
|
|
||||||
"condition_night_4d": {
|
|
||||||
"name": "Condition night 4"
|
|
||||||
},
|
},
|
||||||
"dew_point": {
|
"dew_point": {
|
||||||
"name": "Dew point"
|
"name": "Dew point"
|
||||||
},
|
},
|
||||||
"grass_pollen_0d": {
|
"grass_pollen": {
|
||||||
"name": "Grass pollen today",
|
"name": "Grass pollen day {forecast_day}",
|
||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"level": {
|
"level": {
|
||||||
"name": "Level",
|
"name": "Level",
|
||||||
"state": {
|
"state": {
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"grass_pollen_1d": {
|
"hours_of_sun": {
|
||||||
"name": "Grass pollen day 1",
|
"name": "Hours of sun day {forecast_day}"
|
||||||
|
},
|
||||||
|
"mold_pollen": {
|
||||||
|
"name": "Mold pollen day {forecast_day}",
|
||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"grass_pollen_2d": {
|
|
||||||
"name": "Grass pollen day 2",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"grass_pollen_3d": {
|
|
||||||
"name": "Grass pollen day 3",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"grass_pollen_4d": {
|
|
||||||
"name": "Grass pollen day 4",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hours_of_sun_0d": {
|
|
||||||
"name": "Hours of sun today"
|
|
||||||
},
|
|
||||||
"hours_of_sun_1d": {
|
|
||||||
"name": "Hours of sun day 1"
|
|
||||||
},
|
|
||||||
"hours_of_sun_2d": {
|
|
||||||
"name": "Hours of sun day 2"
|
|
||||||
},
|
|
||||||
"hours_of_sun_3d": {
|
|
||||||
"name": "Hours of sun day 3"
|
|
||||||
},
|
|
||||||
"hours_of_sun_4d": {
|
|
||||||
"name": "Hours of sun day 4"
|
|
||||||
},
|
|
||||||
"mold_pollen_0d": {
|
|
||||||
"name": "Mold pollen today",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mold_pollen_1d": {
|
|
||||||
"name": "Mold pollen day 1",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mold_pollen_2d": {
|
|
||||||
"name": "Mold pollen day 2",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mold_pollen_3d": {
|
|
||||||
"name": "Mold pollen day 3",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mold_pollen_4d": {
|
|
||||||
"name": "Mold pollen day 4",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -334,82 +102,18 @@
|
|||||||
"falling": "Falling"
|
"falling": "Falling"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ragweed_pollen_0d": {
|
"ragweed_pollen": {
|
||||||
"name": "Ragweed pollen today",
|
"name": "Ragweed pollen day {forecast_day}",
|
||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ragweed_pollen_1d": {
|
|
||||||
"name": "Ragweed pollen day 1",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ragweed_pollen_2d": {
|
|
||||||
"name": "Ragweed pollen day 2",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ragweed_pollen_3d": {
|
|
||||||
"name": "Ragweed pollen day 3",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ragweed_pollen_4d": {
|
|
||||||
"name": "Ragweed pollen day 4",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -417,205 +121,45 @@
|
|||||||
"realfeel_temperature": {
|
"realfeel_temperature": {
|
||||||
"name": "RealFeel temperature"
|
"name": "RealFeel temperature"
|
||||||
},
|
},
|
||||||
"realfeel_temperature_max_0d": {
|
"realfeel_temperature_max": {
|
||||||
"name": "RealFeel temperature max today"
|
"name": "RealFeel temperature max day {forecast_day}"
|
||||||
},
|
},
|
||||||
"realfeel_temperature_max_1d": {
|
"realfeel_temperature_min": {
|
||||||
"name": "RealFeel temperature max day 1"
|
"name": "RealFeel temperature min day {forecast_day}"
|
||||||
},
|
|
||||||
"realfeel_temperature_max_2d": {
|
|
||||||
"name": "RealFeel temperature max day 2"
|
|
||||||
},
|
|
||||||
"realfeel_temperature_max_3d": {
|
|
||||||
"name": "RealFeel temperature max day 3"
|
|
||||||
},
|
|
||||||
"realfeel_temperature_max_4d": {
|
|
||||||
"name": "RealFeel temperature max day 4"
|
|
||||||
},
|
|
||||||
"realfeel_temperature_min_0d": {
|
|
||||||
"name": "RealFeel temperature min today"
|
|
||||||
},
|
|
||||||
"realfeel_temperature_min_1d": {
|
|
||||||
"name": "RealFeel temperature min day 1"
|
|
||||||
},
|
|
||||||
"realfeel_temperature_min_2d": {
|
|
||||||
"name": "RealFeel temperature min day 2"
|
|
||||||
},
|
|
||||||
"realfeel_temperature_min_3d": {
|
|
||||||
"name": "RealFeel temperature min day 3"
|
|
||||||
},
|
|
||||||
"realfeel_temperature_min_4d": {
|
|
||||||
"name": "RealFeel temperature min day 4"
|
|
||||||
},
|
},
|
||||||
"realfeel_temperature_shade": {
|
"realfeel_temperature_shade": {
|
||||||
"name": "RealFeel temperature shade"
|
"name": "RealFeel temperature shade"
|
||||||
},
|
},
|
||||||
"realfeel_temperature_shade_max_0d": {
|
"realfeel_temperature_shade_max": {
|
||||||
"name": "RealFeel temperature shade max today"
|
"name": "RealFeel temperature shade max day {forecast_day}"
|
||||||
},
|
},
|
||||||
"realfeel_temperature_shade_max_1d": {
|
"realfeel_temperature_shade_min": {
|
||||||
"name": "RealFeel temperature shade max day 1"
|
"name": "RealFeel temperature shade min day {forecast_day}"
|
||||||
},
|
},
|
||||||
"realfeel_temperature_shade_max_2d": {
|
"solar_irradiance_day": {
|
||||||
"name": "RealFeel temperature shade max day 2"
|
"name": "Solar irradiance day {forecast_day}"
|
||||||
},
|
},
|
||||||
"realfeel_temperature_shade_max_3d": {
|
"solar_irradiance_night": {
|
||||||
"name": "RealFeel temperature shade max day 3"
|
"name": "Solar irradiance night {forecast_day}"
|
||||||
},
|
},
|
||||||
"realfeel_temperature_shade_max_4d": {
|
"thunderstorm_probability_day": {
|
||||||
"name": "RealFeel temperature shade max day 4"
|
"name": "Thunderstorm probability day {forecast_day}"
|
||||||
},
|
},
|
||||||
"realfeel_temperature_shade_min_0d": {
|
"thunderstorm_probability_night": {
|
||||||
"name": "RealFeel temperature shade min today"
|
"name": "Thunderstorm probability night {forecast_day}"
|
||||||
},
|
},
|
||||||
"realfeel_temperature_shade_min_1d": {
|
"tree_pollen": {
|
||||||
"name": "RealFeel temperature shade min day 1"
|
"name": "Tree pollen day {forecast_day}",
|
||||||
},
|
|
||||||
"realfeel_temperature_shade_min_2d": {
|
|
||||||
"name": "RealFeel temperature shade min day 2"
|
|
||||||
},
|
|
||||||
"realfeel_temperature_shade_min_3d": {
|
|
||||||
"name": "RealFeel temperature shade min day 3"
|
|
||||||
},
|
|
||||||
"realfeel_temperature_shade_min_4d": {
|
|
||||||
"name": "RealFeel temperature shade min day 4"
|
|
||||||
},
|
|
||||||
"solar_irradiance_day_0d": {
|
|
||||||
"name": "Solar irradiance today"
|
|
||||||
},
|
|
||||||
"solar_irradiance_day_1d": {
|
|
||||||
"name": "Solar irradiance day 1"
|
|
||||||
},
|
|
||||||
"solar_irradiance_day_2d": {
|
|
||||||
"name": "Solar irradiance day 2"
|
|
||||||
},
|
|
||||||
"solar_irradiance_day_3d": {
|
|
||||||
"name": "Solar irradiance day 3"
|
|
||||||
},
|
|
||||||
"solar_irradiance_day_4d": {
|
|
||||||
"name": "Solar irradiance day 4"
|
|
||||||
},
|
|
||||||
"solar_irradiance_night_0d": {
|
|
||||||
"name": "Solar irradiance tonight"
|
|
||||||
},
|
|
||||||
"solar_irradiance_night_1d": {
|
|
||||||
"name": "Solar irradiance night 1"
|
|
||||||
},
|
|
||||||
"solar_irradiance_night_2d": {
|
|
||||||
"name": "Solar irradiance night 2"
|
|
||||||
},
|
|
||||||
"solar_irradiance_night_3d": {
|
|
||||||
"name": "Solar irradiance night 3"
|
|
||||||
},
|
|
||||||
"solar_irradiance_night_4d": {
|
|
||||||
"name": "Solar irradiance night 4"
|
|
||||||
},
|
|
||||||
"thunderstorm_probability_day_0d": {
|
|
||||||
"name": "Thunderstorm probability today"
|
|
||||||
},
|
|
||||||
"thunderstorm_probability_day_1d": {
|
|
||||||
"name": "Thunderstorm probability day 1"
|
|
||||||
},
|
|
||||||
"thunderstorm_probability_day_2d": {
|
|
||||||
"name": "Thunderstorm probability day 2"
|
|
||||||
},
|
|
||||||
"thunderstorm_probability_day_3d": {
|
|
||||||
"name": "Thunderstorm probability day 3"
|
|
||||||
},
|
|
||||||
"thunderstorm_probability_day_4d": {
|
|
||||||
"name": "Thunderstorm probability day 4"
|
|
||||||
},
|
|
||||||
"thunderstorm_probability_night_0d": {
|
|
||||||
"name": "Thunderstorm probability tonight"
|
|
||||||
},
|
|
||||||
"thunderstorm_probability_night_1d": {
|
|
||||||
"name": "Thunderstorm probability night 1"
|
|
||||||
},
|
|
||||||
"thunderstorm_probability_night_2d": {
|
|
||||||
"name": "Thunderstorm probability night 2"
|
|
||||||
},
|
|
||||||
"thunderstorm_probability_night_3d": {
|
|
||||||
"name": "Thunderstorm probability night 3"
|
|
||||||
},
|
|
||||||
"thunderstorm_probability_night_4d": {
|
|
||||||
"name": "Thunderstorm probability night 4"
|
|
||||||
},
|
|
||||||
"tree_pollen_0d": {
|
|
||||||
"name": "Tree pollen today",
|
|
||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tree_pollen_1d": {
|
|
||||||
"name": "Tree pollen day 1",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tree_pollen_2d": {
|
|
||||||
"name": "Tree pollen day 2",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tree_pollen_3d": {
|
|
||||||
"name": "Tree pollen day 3",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tree_pollen_4d": {
|
|
||||||
"name": "Tree pollen day 4",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -624,94 +168,30 @@
|
|||||||
"name": "UV index",
|
"name": "UV index",
|
||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uv_index_0d": {
|
"uv_index_forecast": {
|
||||||
"name": "UV index today",
|
"name": "UV index day {forecast_day}",
|
||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"level": {
|
"level": {
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uv_index_1d": {
|
|
||||||
"name": "UV index day 1",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uv_index_2d": {
|
|
||||||
"name": "UV index day 2",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uv_index_3d": {
|
|
||||||
"name": "UV index day 3",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uv_index_4d": {
|
|
||||||
"name": "UV index day 4",
|
|
||||||
"state_attributes": {
|
|
||||||
"level": {
|
|
||||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
|
|
||||||
"state": {
|
|
||||||
"good": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::good%]",
|
|
||||||
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::hazardous%]",
|
|
||||||
"high": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::high%]",
|
|
||||||
"low": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::low%]",
|
|
||||||
"moderate": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::moderate%]",
|
|
||||||
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality_0d::state::unhealthy%]"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -728,65 +208,17 @@
|
|||||||
"wind_gust_speed": {
|
"wind_gust_speed": {
|
||||||
"name": "[%key:component::weather::entity_component::_::state_attributes::wind_gust_speed::name%]"
|
"name": "[%key:component::weather::entity_component::_::state_attributes::wind_gust_speed::name%]"
|
||||||
},
|
},
|
||||||
"wind_gust_speed_day_0d": {
|
"wind_gust_speed_day": {
|
||||||
"name": "Wind gust speed today"
|
"name": "Wind gust speed day {forecast_day}"
|
||||||
},
|
},
|
||||||
"wind_gust_speed_day_1d": {
|
"wind_gust_speed_night": {
|
||||||
"name": "Wind gust speed day 1"
|
"name": "Wind gust speed night {forecast_day}"
|
||||||
},
|
},
|
||||||
"wind_gust_speed_day_2d": {
|
"wind_speed_day": {
|
||||||
"name": "Wind gust speed day 2"
|
"name": "Wind speed day {forecast_day}"
|
||||||
},
|
},
|
||||||
"wind_gust_speed_day_3d": {
|
"wind_speed_night": {
|
||||||
"name": "Wind gust speed day 3"
|
"name": "Wind speed night {forecast_day}"
|
||||||
},
|
|
||||||
"wind_gust_speed_day_4d": {
|
|
||||||
"name": "Wind gust speed day 4"
|
|
||||||
},
|
|
||||||
"wind_gust_speed_night_0d": {
|
|
||||||
"name": "Wind gust speed tonight"
|
|
||||||
},
|
|
||||||
"wind_gust_speed_night_1d": {
|
|
||||||
"name": "Wind gust speed night 1"
|
|
||||||
},
|
|
||||||
"wind_gust_speed_night_2d": {
|
|
||||||
"name": "Wind gust speed night 2"
|
|
||||||
},
|
|
||||||
"wind_gust_speed_night_3d": {
|
|
||||||
"name": "Wind gust speed night 3"
|
|
||||||
},
|
|
||||||
"wind_gust_speed_night_4d": {
|
|
||||||
"name": "Wind gust speed night 4"
|
|
||||||
},
|
|
||||||
"wind_speed_day_0d": {
|
|
||||||
"name": "Wind speed today"
|
|
||||||
},
|
|
||||||
"wind_speed_day_1d": {
|
|
||||||
"name": "Wind speed day 1"
|
|
||||||
},
|
|
||||||
"wind_speed_day_2d": {
|
|
||||||
"name": "Wind speed day 2"
|
|
||||||
},
|
|
||||||
"wind_speed_day_3d": {
|
|
||||||
"name": "Wind speed day 3"
|
|
||||||
},
|
|
||||||
"wind_speed_day_4d": {
|
|
||||||
"name": "Wind speed day 4"
|
|
||||||
},
|
|
||||||
"wind_speed_night_0d": {
|
|
||||||
"name": "Wind speed tonight"
|
|
||||||
},
|
|
||||||
"wind_speed_night_1d": {
|
|
||||||
"name": "Wind speed night 1"
|
|
||||||
},
|
|
||||||
"wind_speed_night_2d": {
|
|
||||||
"name": "Wind speed night 2"
|
|
||||||
},
|
|
||||||
"wind_speed_night_3d": {
|
|
||||||
"name": "Wind speed night 3"
|
|
||||||
},
|
|
||||||
"wind_speed_night_4d": {
|
|
||||||
"name": "Wind speed night 4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -9,6 +9,7 @@ from accuweather.const import ENDPOINT
|
|||||||
from homeassistant.components import system_health
|
from homeassistant.components import system_health
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from . import AccuWeatherConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
@ -22,9 +23,11 @@ def async_register(
|
|||||||
|
|
||||||
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
||||||
"""Get info for the info page."""
|
"""Get info for the info page."""
|
||||||
remaining_requests = list(hass.data[DOMAIN].values())[
|
config_entry: AccuWeatherConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||||
0
|
|
||||||
].coordinator_observation.accuweather.requests_remaining
|
remaining_requests = (
|
||||||
|
config_entry.runtime_data.coordinator_observation.accuweather.requests_remaining
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT),
|
"can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT),
|
||||||
|
@ -7,6 +7,7 @@ from typing import cast
|
|||||||
from homeassistant.components.weather import (
|
from homeassistant.components.weather import (
|
||||||
ATTR_FORECAST_CLOUD_COVERAGE,
|
ATTR_FORECAST_CLOUD_COVERAGE,
|
||||||
ATTR_FORECAST_CONDITION,
|
ATTR_FORECAST_CONDITION,
|
||||||
|
ATTR_FORECAST_HUMIDITY,
|
||||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP,
|
ATTR_FORECAST_NATIVE_APPARENT_TEMP,
|
||||||
ATTR_FORECAST_NATIVE_PRECIPITATION,
|
ATTR_FORECAST_NATIVE_PRECIPITATION,
|
||||||
ATTR_FORECAST_NATIVE_TEMP,
|
ATTR_FORECAST_NATIVE_TEMP,
|
||||||
@ -21,7 +22,6 @@ from homeassistant.components.weather import (
|
|||||||
Forecast,
|
Forecast,
|
||||||
WeatherEntityFeature,
|
WeatherEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
UnitOfLength,
|
UnitOfLength,
|
||||||
UnitOfPrecipitationDepth,
|
UnitOfPrecipitationDepth,
|
||||||
@ -33,7 +33,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util.dt import utc_from_timestamp
|
from homeassistant.util.dt import utc_from_timestamp
|
||||||
|
|
||||||
from . import AccuWeatherData
|
from . import AccuWeatherConfigEntry, AccuWeatherData
|
||||||
from .const import (
|
from .const import (
|
||||||
API_METRIC,
|
API_METRIC,
|
||||||
ATTR_DIRECTION,
|
ATTR_DIRECTION,
|
||||||
@ -41,7 +41,6 @@ from .const import (
|
|||||||
ATTR_VALUE,
|
ATTR_VALUE,
|
||||||
ATTRIBUTION,
|
ATTRIBUTION,
|
||||||
CONDITION_MAP,
|
CONDITION_MAP,
|
||||||
DOMAIN,
|
|
||||||
)
|
)
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
AccuWeatherDailyForecastDataUpdateCoordinator,
|
AccuWeatherDailyForecastDataUpdateCoordinator,
|
||||||
@ -52,12 +51,12 @@ PARALLEL_UPDATES = 1
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: AccuWeatherConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a AccuWeather weather entity from a config_entry."""
|
"""Add a AccuWeather weather entity from a config_entry."""
|
||||||
accuweather_data: AccuWeatherData = hass.data[DOMAIN][entry.entry_id]
|
async_add_entities([AccuWeatherEntity(entry.runtime_data)])
|
||||||
|
|
||||||
async_add_entities([AccuWeatherEntity(accuweather_data)])
|
|
||||||
|
|
||||||
|
|
||||||
class AccuWeatherEntity(
|
class AccuWeatherEntity(
|
||||||
@ -185,6 +184,7 @@ class AccuWeatherEntity(
|
|||||||
{
|
{
|
||||||
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
|
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
|
||||||
ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCoverDay"],
|
ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCoverDay"],
|
||||||
|
ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"]["Average"],
|
||||||
ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"][ATTR_VALUE],
|
ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"][ATTR_VALUE],
|
||||||
ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"][ATTR_VALUE],
|
ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"][ATTR_VALUE],
|
||||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperatureMax"][
|
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperatureMax"][
|
||||||
|
@ -10,7 +10,7 @@ CONF_HUBS = "hubs"
|
|||||||
|
|
||||||
PLATFORMS = [Platform.COVER, Platform.SENSOR]
|
PLATFORMS = [Platform.COVER, Platform.SENSOR]
|
||||||
|
|
||||||
AcmedaConfigEntry = ConfigEntry[PulseHub]
|
type AcmedaConfigEntry = ConfigEntry[PulseHub]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@ -43,7 +43,7 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
||||||
AdGuardConfigEntry = ConfigEntry["AdGuardData"]
|
type AdGuardConfigEntry = ConfigEntry[AdGuardData]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ads",
|
"documentation": "https://www.home-assistant.io/integrations/ads",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyads"],
|
"loggers": ["pyads"],
|
||||||
"requirements": ["pyads==3.2.2"]
|
"requirements": ["pyads==3.4.0"]
|
||||||
}
|
}
|
||||||
|
@ -12,9 +12,11 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
from homeassistant.helpers.debounce import Debouncer
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import ADVANTAGE_AIR_RETRY, DOMAIN
|
from .const import ADVANTAGE_AIR_RETRY
|
||||||
from .models import AdvantageAirData
|
from .models import AdvantageAirData
|
||||||
|
|
||||||
|
type AdvantageAirDataConfigEntry = ConfigEntry[AdvantageAirData]
|
||||||
|
|
||||||
ADVANTAGE_AIR_SYNC_INTERVAL = 15
|
ADVANTAGE_AIR_SYNC_INTERVAL = 15
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
@ -31,7 +33,9 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
REQUEST_REFRESH_DELAY = 0.5
|
REQUEST_REFRESH_DELAY = 0.5
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: AdvantageAirDataConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Set up Advantage Air config."""
|
"""Set up Advantage Air config."""
|
||||||
ip_address = entry.data[CONF_IP_ADDRESS]
|
ip_address = entry.data[CONF_IP_ADDRESS]
|
||||||
port = entry.data[CONF_PORT]
|
port = entry.data[CONF_PORT]
|
||||||
@ -61,19 +65,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
entry.runtime_data = AdvantageAirData(coordinator, api)
|
||||||
hass.data[DOMAIN][entry.entry_id] = AdvantageAirData(coordinator, api)
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: AdvantageAirDataConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload Advantage Air Config."""
|
"""Unload Advantage Air Config."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
@ -6,12 +6,11 @@ from homeassistant.components.binary_sensor import (
|
|||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
from . import AdvantageAirDataConfigEntry
|
||||||
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
|
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
|
||||||
from .models import AdvantageAirData
|
from .models import AdvantageAirData
|
||||||
|
|
||||||
@ -20,12 +19,12 @@ PARALLEL_UPDATES = 0
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AdvantageAirDataConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AdvantageAir Binary Sensor platform."""
|
"""Set up AdvantageAir Binary Sensor platform."""
|
||||||
|
|
||||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
instance = config_entry.runtime_data
|
||||||
|
|
||||||
entities: list[BinarySensorEntity] = []
|
entities: list[BinarySensorEntity] = []
|
||||||
if aircons := instance.coordinator.data.get("aircons"):
|
if aircons := instance.coordinator.data.get("aircons"):
|
||||||
|
@ -16,19 +16,18 @@ from homeassistant.components.climate import (
|
|||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
|
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AdvantageAirDataConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
||||||
ADVANTAGE_AIR_STATE_CLOSE,
|
ADVANTAGE_AIR_STATE_CLOSE,
|
||||||
ADVANTAGE_AIR_STATE_OFF,
|
ADVANTAGE_AIR_STATE_OFF,
|
||||||
ADVANTAGE_AIR_STATE_ON,
|
ADVANTAGE_AIR_STATE_ON,
|
||||||
ADVANTAGE_AIR_STATE_OPEN,
|
ADVANTAGE_AIR_STATE_OPEN,
|
||||||
DOMAIN as ADVANTAGE_AIR_DOMAIN,
|
|
||||||
)
|
)
|
||||||
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
|
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
|
||||||
from .models import AdvantageAirData
|
from .models import AdvantageAirData
|
||||||
@ -76,12 +75,12 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AdvantageAirDataConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AdvantageAir climate platform."""
|
"""Set up AdvantageAir climate platform."""
|
||||||
|
|
||||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
instance = config_entry.runtime_data
|
||||||
|
|
||||||
entities: list[ClimateEntity] = []
|
entities: list[ClimateEntity] = []
|
||||||
if aircons := instance.coordinator.data.get("aircons"):
|
if aircons := instance.coordinator.data.get("aircons"):
|
||||||
|
@ -8,15 +8,11 @@ from homeassistant.components.cover import (
|
|||||||
CoverEntity,
|
CoverEntity,
|
||||||
CoverEntityFeature,
|
CoverEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from . import AdvantageAirDataConfigEntry
|
||||||
ADVANTAGE_AIR_STATE_CLOSE,
|
from .const import ADVANTAGE_AIR_STATE_CLOSE, ADVANTAGE_AIR_STATE_OPEN
|
||||||
ADVANTAGE_AIR_STATE_OPEN,
|
|
||||||
DOMAIN as ADVANTAGE_AIR_DOMAIN,
|
|
||||||
)
|
|
||||||
from .entity import AdvantageAirThingEntity, AdvantageAirZoneEntity
|
from .entity import AdvantageAirThingEntity, AdvantageAirZoneEntity
|
||||||
from .models import AdvantageAirData
|
from .models import AdvantageAirData
|
||||||
|
|
||||||
@ -25,12 +21,12 @@ PARALLEL_UPDATES = 0
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AdvantageAirDataConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AdvantageAir cover platform."""
|
"""Set up AdvantageAir cover platform."""
|
||||||
|
|
||||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
instance = config_entry.runtime_data
|
||||||
|
|
||||||
entities: list[CoverEntity] = []
|
entities: list[CoverEntity] = []
|
||||||
if aircons := instance.coordinator.data.get("aircons"):
|
if aircons := instance.coordinator.data.get("aircons"):
|
||||||
|
@ -5,10 +5,9 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
from . import AdvantageAirDataConfigEntry
|
||||||
|
|
||||||
TO_REDACT = [
|
TO_REDACT = [
|
||||||
"dealerPhoneNumber",
|
"dealerPhoneNumber",
|
||||||
@ -25,10 +24,10 @@ TO_REDACT = [
|
|||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, config_entry: ConfigEntry
|
hass: HomeAssistant, config_entry: AdvantageAirDataConfigEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id].coordinator.data
|
data = config_entry.runtime_data.coordinator.data
|
||||||
|
|
||||||
# Return only the relevant children
|
# Return only the relevant children
|
||||||
return {
|
return {
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AdvantageAirDataConfigEntry
|
||||||
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN
|
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||||
from .entity import AdvantageAirEntity, AdvantageAirThingEntity
|
from .entity import AdvantageAirEntity, AdvantageAirThingEntity
|
||||||
from .models import AdvantageAirData
|
from .models import AdvantageAirData
|
||||||
@ -15,12 +15,12 @@ from .models import AdvantageAirData
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AdvantageAirDataConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AdvantageAir light platform."""
|
"""Set up AdvantageAir light platform."""
|
||||||
|
|
||||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
instance = config_entry.runtime_data
|
||||||
|
|
||||||
entities: list[LightEntity] = []
|
entities: list[LightEntity] = []
|
||||||
if my_lights := instance.coordinator.data.get("myLights"):
|
if my_lights := instance.coordinator.data.get("myLights"):
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
"""Select platform for Advantage Air integration."""
|
"""Select platform for Advantage Air integration."""
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity
|
from homeassistant.components.select import SelectEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
from . import AdvantageAirDataConfigEntry
|
||||||
from .entity import AdvantageAirAcEntity
|
from .entity import AdvantageAirAcEntity
|
||||||
from .models import AdvantageAirData
|
from .models import AdvantageAirData
|
||||||
|
|
||||||
@ -14,12 +13,12 @@ ADVANTAGE_AIR_INACTIVE = "Inactive"
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AdvantageAirDataConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AdvantageAir select platform."""
|
"""Set up AdvantageAir select platform."""
|
||||||
|
|
||||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
instance = config_entry.runtime_data
|
||||||
|
|
||||||
if aircons := instance.coordinator.data.get("aircons"):
|
if aircons := instance.coordinator.data.get("aircons"):
|
||||||
async_add_entities(AdvantageAirMyZone(instance, ac_key) for ac_key in aircons)
|
async_add_entities(AdvantageAirMyZone(instance, ac_key) for ac_key in aircons)
|
||||||
|
@ -12,13 +12,13 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
|
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN
|
from . import AdvantageAirDataConfigEntry
|
||||||
|
from .const import ADVANTAGE_AIR_STATE_OPEN
|
||||||
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
|
from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity
|
||||||
from .models import AdvantageAirData
|
from .models import AdvantageAirData
|
||||||
|
|
||||||
@ -31,12 +31,12 @@ PARALLEL_UPDATES = 0
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AdvantageAirDataConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AdvantageAir sensor platform."""
|
"""Set up AdvantageAir sensor platform."""
|
||||||
|
|
||||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
instance = config_entry.runtime_data
|
||||||
|
|
||||||
entities: list[SensorEntity] = []
|
entities: list[SensorEntity] = []
|
||||||
if aircons := instance.coordinator.data.get("aircons"):
|
if aircons := instance.coordinator.data.get("aircons"):
|
||||||
|
@ -3,15 +3,14 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AdvantageAirDataConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
ADVANTAGE_AIR_AUTOFAN_ENABLED,
|
||||||
ADVANTAGE_AIR_STATE_OFF,
|
ADVANTAGE_AIR_STATE_OFF,
|
||||||
ADVANTAGE_AIR_STATE_ON,
|
ADVANTAGE_AIR_STATE_ON,
|
||||||
DOMAIN as ADVANTAGE_AIR_DOMAIN,
|
|
||||||
)
|
)
|
||||||
from .entity import AdvantageAirAcEntity, AdvantageAirThingEntity
|
from .entity import AdvantageAirAcEntity, AdvantageAirThingEntity
|
||||||
from .models import AdvantageAirData
|
from .models import AdvantageAirData
|
||||||
@ -19,12 +18,12 @@ from .models import AdvantageAirData
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AdvantageAirDataConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AdvantageAir switch platform."""
|
"""Set up AdvantageAir switch platform."""
|
||||||
|
|
||||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
instance = config_entry.runtime_data
|
||||||
|
|
||||||
entities: list[SwitchEntity] = []
|
entities: list[SwitchEntity] = []
|
||||||
if aircons := instance.coordinator.data.get("aircons"):
|
if aircons := instance.coordinator.data.get("aircons"):
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
"""Advantage Air Update platform."""
|
"""Advantage Air Update platform."""
|
||||||
|
|
||||||
from homeassistant.components.update import UpdateEntity
|
from homeassistant.components.update import UpdateEntity
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AdvantageAirDataConfigEntry
|
||||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||||
from .entity import AdvantageAirEntity
|
from .entity import AdvantageAirEntity
|
||||||
from .models import AdvantageAirData
|
from .models import AdvantageAirData
|
||||||
@ -13,12 +13,12 @@ from .models import AdvantageAirData
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AdvantageAirDataConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AdvantageAir update platform."""
|
"""Set up AdvantageAir update platform."""
|
||||||
|
|
||||||
instance: AdvantageAirData = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]
|
instance = config_entry.runtime_data
|
||||||
|
|
||||||
async_add_entities([AdvantageAirApp(instance)])
|
async_add_entities([AdvantageAirApp(instance)])
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""The AEMET OpenData component."""
|
"""The AEMET OpenData component."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aemet_opendata.exceptions import AemetError, TownNotFound
|
from aemet_opendata.exceptions import AemetError, TownNotFound
|
||||||
@ -11,19 +12,23 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
from .const import (
|
from .const import CONF_STATION_UPDATES, PLATFORMS
|
||||||
CONF_STATION_UPDATES,
|
|
||||||
DOMAIN,
|
|
||||||
ENTRY_NAME,
|
|
||||||
ENTRY_WEATHER_COORDINATOR,
|
|
||||||
PLATFORMS,
|
|
||||||
)
|
|
||||||
from .coordinator import WeatherUpdateCoordinator
|
from .coordinator import WeatherUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type AemetConfigEntry = ConfigEntry[AemetData]
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
|
@dataclass
|
||||||
|
class AemetData:
|
||||||
|
"""Aemet runtime data."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
coordinator: WeatherUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> bool:
|
||||||
"""Set up AEMET OpenData as config entry."""
|
"""Set up AEMET OpenData as config entry."""
|
||||||
name = entry.data[CONF_NAME]
|
name = entry.data[CONF_NAME]
|
||||||
api_key = entry.data[CONF_API_KEY]
|
api_key = entry.data[CONF_API_KEY]
|
||||||
@ -44,11 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
weather_coordinator = WeatherUpdateCoordinator(hass, aemet)
|
weather_coordinator = WeatherUpdateCoordinator(hass, aemet)
|
||||||
await weather_coordinator.async_config_entry_first_refresh()
|
await weather_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
entry.runtime_data = AemetData(name=name, coordinator=weather_coordinator)
|
||||||
hass.data[DOMAIN][entry.entry_id] = {
|
|
||||||
ENTRY_NAME: name,
|
|
||||||
ENTRY_WEATHER_COORDINATOR: weather_coordinator,
|
|
||||||
}
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
@ -64,9 +65,4 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
@ -55,8 +55,6 @@ CONF_STATION_UPDATES = "station_updates"
|
|||||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||||
DEFAULT_NAME = "AEMET"
|
DEFAULT_NAME = "AEMET"
|
||||||
DOMAIN = "aemet"
|
DOMAIN = "aemet"
|
||||||
ENTRY_NAME = "name"
|
|
||||||
ENTRY_WEATHER_COORDINATOR = "weather_coordinator"
|
|
||||||
|
|
||||||
ATTR_API_CONDITION = "condition"
|
ATTR_API_CONDITION = "condition"
|
||||||
ATTR_API_FORECAST_CONDITION = "condition"
|
ATTR_API_FORECAST_CONDITION = "condition"
|
||||||
|
@ -7,7 +7,6 @@ from typing import Any
|
|||||||
from aemet_opendata.const import AOD_COORDS
|
from aemet_opendata.const import AOD_COORDS
|
||||||
|
|
||||||
from homeassistant.components.diagnostics.util import async_redact_data
|
from homeassistant.components.diagnostics.util import async_redact_data
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
@ -16,8 +15,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .const import DOMAIN, ENTRY_WEATHER_COORDINATOR
|
from . import AemetConfigEntry
|
||||||
from .coordinator import WeatherUpdateCoordinator
|
|
||||||
|
|
||||||
TO_REDACT_CONFIG = [
|
TO_REDACT_CONFIG = [
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
@ -32,11 +30,10 @@ TO_REDACT_COORD = [
|
|||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, config_entry: ConfigEntry
|
hass: HomeAssistant, config_entry: AemetConfigEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
aemet_entry = hass.data[DOMAIN][config_entry.entry_id]
|
coordinator = config_entry.runtime_data.coordinator
|
||||||
coordinator: WeatherUpdateCoordinator = aemet_entry[ENTRY_WEATHER_COORDINATOR]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"api_data": coordinator.aemet.raw_data(),
|
"api_data": coordinator.aemet.raw_data(),
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aemet_opendata"],
|
"loggers": ["aemet_opendata"],
|
||||||
"requirements": ["AEMET-OpenData==0.5.1"]
|
"requirements": ["AEMET-OpenData==0.5.2"]
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from . import AemetConfigEntry
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_API_CONDITION,
|
ATTR_API_CONDITION,
|
||||||
ATTR_API_FORECAST_CONDITION,
|
ATTR_API_FORECAST_CONDITION,
|
||||||
@ -87,9 +88,6 @@ from .const import (
|
|||||||
ATTR_API_WIND_SPEED,
|
ATTR_API_WIND_SPEED,
|
||||||
ATTRIBUTION,
|
ATTRIBUTION,
|
||||||
CONDITIONS_MAP,
|
CONDITIONS_MAP,
|
||||||
DOMAIN,
|
|
||||||
ENTRY_NAME,
|
|
||||||
ENTRY_WEATHER_COORDINATOR,
|
|
||||||
)
|
)
|
||||||
from .coordinator import WeatherUpdateCoordinator
|
from .coordinator import WeatherUpdateCoordinator
|
||||||
from .entity import AemetEntity
|
from .entity import AemetEntity
|
||||||
@ -360,13 +358,13 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AemetConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AEMET OpenData sensor entities based on a config entry."""
|
"""Set up AEMET OpenData sensor entities based on a config entry."""
|
||||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
domain_data = config_entry.runtime_data
|
||||||
name: str = domain_data[ENTRY_NAME]
|
name = domain_data.name
|
||||||
coordinator: WeatherUpdateCoordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
coordinator = domain_data.coordinator
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
AemetSensor(
|
AemetSensor(
|
||||||
|
@ -18,7 +18,6 @@ from homeassistant.components.weather import (
|
|||||||
SingleCoordinatorWeatherEntity,
|
SingleCoordinatorWeatherEntity,
|
||||||
WeatherEntityFeature,
|
WeatherEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
UnitOfPrecipitationDepth,
|
UnitOfPrecipitationDepth,
|
||||||
UnitOfPressure,
|
UnitOfPressure,
|
||||||
@ -28,32 +27,24 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from . import AemetConfigEntry
|
||||||
ATTRIBUTION,
|
from .const import ATTRIBUTION, CONDITIONS_MAP
|
||||||
CONDITIONS_MAP,
|
|
||||||
DOMAIN,
|
|
||||||
ENTRY_NAME,
|
|
||||||
ENTRY_WEATHER_COORDINATOR,
|
|
||||||
)
|
|
||||||
from .coordinator import WeatherUpdateCoordinator
|
from .coordinator import WeatherUpdateCoordinator
|
||||||
from .entity import AemetEntity
|
from .entity import AemetEntity
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AemetConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up AEMET OpenData weather entity based on a config entry."""
|
"""Set up AEMET OpenData weather entity based on a config entry."""
|
||||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
domain_data = config_entry.runtime_data
|
||||||
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
|
name = domain_data.name
|
||||||
|
weather_coordinator = domain_data.coordinator
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[AemetWeather(name, config_entry.unique_id, weather_coordinator)],
|
||||||
AemetWeather(
|
|
||||||
domain_data[ENTRY_NAME], config_entry.unique_id, weather_coordinator
|
|
||||||
)
|
|
||||||
],
|
|
||||||
False,
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
AfterShipConfigEntry = ConfigEntry[AfterShip]
|
type AfterShipConfigEntry = ConfigEntry[AfterShip]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AfterShipConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AfterShipConfigEntry) -> bool:
|
||||||
|
@ -10,18 +10,20 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
|||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import CONNECTION, DOMAIN as AGENT_DOMAIN, SERVER_URL
|
from .const import DOMAIN as AGENT_DOMAIN, SERVER_URL
|
||||||
|
|
||||||
ATTRIBUTION = "ispyconnect.com"
|
ATTRIBUTION = "ispyconnect.com"
|
||||||
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
|
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
|
||||||
|
|
||||||
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
|
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
|
||||||
|
|
||||||
|
AgentDVRConfigEntry = ConfigEntry[Agent]
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, config_entry: AgentDVRConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Set up the Agent component."""
|
"""Set up the Agent component."""
|
||||||
hass.data.setdefault(AGENT_DOMAIN, {})
|
|
||||||
|
|
||||||
server_origin = config_entry.data[SERVER_URL]
|
server_origin = config_entry.data[SERVER_URL]
|
||||||
|
|
||||||
agent_client = Agent(server_origin, async_get_clientsession(hass))
|
agent_client = Agent(server_origin, async_get_clientsession(hass))
|
||||||
@ -34,9 +36,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
if not agent_client.is_available:
|
if not agent_client.is_available:
|
||||||
raise ConfigEntryNotReady
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
config_entry.async_on_unload(agent_client.close)
|
||||||
|
|
||||||
await agent_client.get_devices()
|
await agent_client.get_devices()
|
||||||
|
|
||||||
hass.data[AGENT_DOMAIN][config_entry.entry_id] = {CONNECTION: agent_client}
|
config_entry.runtime_data = agent_client
|
||||||
|
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
@ -54,15 +58,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, config_entry: AgentDVRConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||||
config_entry, PLATFORMS
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION].close()
|
|
||||||
|
|
||||||
if unload_ok:
|
|
||||||
hass.data[AGENT_DOMAIN].pop(config_entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
@ -6,7 +6,6 @@ from homeassistant.components.alarm_control_panel import (
|
|||||||
AlarmControlPanelEntity,
|
AlarmControlPanelEntity,
|
||||||
AlarmControlPanelEntityFeature,
|
AlarmControlPanelEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY,
|
STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_ALARM_ARMED_HOME,
|
STATE_ALARM_ARMED_HOME,
|
||||||
@ -17,7 +16,8 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import CONNECTION, DOMAIN as AGENT_DOMAIN
|
from . import AgentDVRConfigEntry
|
||||||
|
from .const import DOMAIN as AGENT_DOMAIN
|
||||||
|
|
||||||
CONF_HOME_MODE_NAME = "home"
|
CONF_HOME_MODE_NAME = "home"
|
||||||
CONF_AWAY_MODE_NAME = "away"
|
CONF_AWAY_MODE_NAME = "away"
|
||||||
@ -28,13 +28,11 @@ CONST_ALARM_CONTROL_PANEL_NAME = "Alarm Panel"
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AgentDVRConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Agent DVR Alarm Control Panels."""
|
"""Set up the Agent DVR Alarm Control Panels."""
|
||||||
async_add_entities(
|
async_add_entities([AgentBaseStation(config_entry.runtime_data)])
|
||||||
[AgentBaseStation(hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION])]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AgentBaseStation(AlarmControlPanelEntity):
|
class AgentBaseStation(AlarmControlPanelEntity):
|
||||||
@ -45,6 +43,7 @@ class AgentBaseStation(AlarmControlPanelEntity):
|
|||||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||||
| AlarmControlPanelEntityFeature.ARM_NIGHT
|
| AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||||
)
|
)
|
||||||
|
_attr_code_arm_required = False
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ from agent import AgentError
|
|||||||
|
|
||||||
from homeassistant.components.camera import CameraEntityFeature
|
from homeassistant.components.camera import CameraEntityFeature
|
||||||
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import (
|
from homeassistant.helpers.entity_platform import (
|
||||||
@ -15,12 +14,8 @@ from homeassistant.helpers.entity_platform import (
|
|||||||
async_get_current_platform,
|
async_get_current_platform,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import (
|
from . import AgentDVRConfigEntry
|
||||||
ATTRIBUTION,
|
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN
|
||||||
CAMERA_SCAN_INTERVAL_SECS,
|
|
||||||
CONNECTION,
|
|
||||||
DOMAIN as AGENT_DOMAIN,
|
|
||||||
)
|
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
|
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
|
||||||
|
|
||||||
@ -43,14 +38,14 @@ CAMERA_SERVICES = {
|
|||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: AgentDVRConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Agent cameras."""
|
"""Set up the Agent cameras."""
|
||||||
filter_urllib3_logging()
|
filter_urllib3_logging()
|
||||||
cameras = []
|
cameras = []
|
||||||
|
|
||||||
server = hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION]
|
server = config_entry.runtime_data
|
||||||
if not server.devices:
|
if not server.devices:
|
||||||
_LOGGER.warning("Could not fetch cameras from Agent server")
|
_LOGGER.warning("Could not fetch cameras from Agent server")
|
||||||
return
|
return
|
||||||
@ -80,11 +75,11 @@ class AgentCamera(MjpegCamera):
|
|||||||
"""Initialize as a subclass of MjpegCamera."""
|
"""Initialize as a subclass of MjpegCamera."""
|
||||||
self.device = device
|
self.device = device
|
||||||
self._removed = False
|
self._removed = False
|
||||||
self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}"
|
self._attr_unique_id = f"{device.client.unique}_{device.typeID}_{device.id}"
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=device.name,
|
name=device.name,
|
||||||
mjpeg_url=f"{device.client._server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}",
|
mjpeg_url=f"{device.client._server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001
|
||||||
still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}",
|
still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001
|
||||||
)
|
)
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(AGENT_DOMAIN, self.unique_id)},
|
identifiers={(AGENT_DOMAIN, self.unique_id)},
|
||||||
|
@ -9,4 +9,3 @@ SERVICE_UPDATE = "update"
|
|||||||
SIGNAL_UPDATE_AGENT = "agent_update"
|
SIGNAL_UPDATE_AGENT = "agent_update"
|
||||||
ATTRIBUTION = "Data provided by ispyconnect.com"
|
ATTRIBUTION = "Data provided by ispyconnect.com"
|
||||||
SERVER_URL = "server_url"
|
SERVER_URL = "server_url"
|
||||||
CONNECTION = "connection"
|
|
||||||
|
@ -17,7 +17,6 @@ from homeassistant.helpers.entity import Entity
|
|||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.typing import ConfigType, StateType
|
from homeassistant.helpers.typing import ConfigType, StateType
|
||||||
|
|
||||||
from . import group as group_pre_import # noqa: F401
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER: Final = logging.getLogger(__name__)
|
_LOGGER: Final = logging.getLogger(__name__)
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
"""Describe group states."""
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
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"
|
|
||||||
) -> None:
|
|
||||||
"""Describe group on off states."""
|
|
||||||
registry.exclude_domain(DOMAIN)
|
|
67
homeassistant/components/airgradient/__init__.py
Normal file
67
homeassistant/components/airgradient/__init__.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
"""The Airgradient integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AirGradientData:
|
||||||
|
"""AirGradient data class."""
|
||||||
|
|
||||||
|
measurement: AirGradientMeasurementCoordinator
|
||||||
|
config: AirGradientConfigCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
type AirGradientConfigEntry = ConfigEntry[AirGradientData]
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
entry.runtime_data = AirGradientData(
|
||||||
|
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."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
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__)
|
62
homeassistant/components/airgradient/coordinator.py
Normal file
62
homeassistant/components/airgradient/coordinator.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""Define an object to manage fetching AirGradient data."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from airgradient import AirGradientClient, AirGradientError, Config, Measures
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import LOGGER
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import AirGradientConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
class AirGradientCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||||
|
"""Class to manage fetching AirGradient data."""
|
||||||
|
|
||||||
|
_update_interval: timedelta
|
||||||
|
config_entry: AirGradientConfigEntry
|
||||||
|
|
||||||
|
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.6.0"],
|
||||||
|
"zeroconf": ["_airgradient._tcp.local."]
|
||||||
|
}
|
157
homeassistant/components/airgradient/select.py
Normal file
157
homeassistant/components/airgradient/select.py
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
"""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,
|
||||||
|
LedBarMode,
|
||||||
|
PmStandard,
|
||||||
|
TemperatureUnit,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import AirGradientConfigEntry
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AirGradientConfigCoordinator
|
||||||
|
from .entity import AirGradientEntity
|
||||||
|
|
||||||
|
PM_STANDARD = {
|
||||||
|
PmStandard.UGM3: "ugm3",
|
||||||
|
PmStandard.USAQI: "us_aqi",
|
||||||
|
}
|
||||||
|
PM_STANDARD_REVERSE = {v: k for k, v in PM_STANDARD.items()}
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
requires_led_bar: 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,
|
||||||
|
),
|
||||||
|
AirGradientSelectEntityDescription(
|
||||||
|
key="display_pm_standard",
|
||||||
|
translation_key="display_pm_standard",
|
||||||
|
options=list(PM_STANDARD_REVERSE),
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
value_fn=lambda config: PM_STANDARD.get(config.pm_standard),
|
||||||
|
set_value_fn=lambda client, value: client.set_pm_standard(
|
||||||
|
PM_STANDARD_REVERSE[value]
|
||||||
|
),
|
||||||
|
requires_display=True,
|
||||||
|
),
|
||||||
|
AirGradientSelectEntityDescription(
|
||||||
|
key="led_bar_mode",
|
||||||
|
translation_key="led_bar_mode",
|
||||||
|
options=[x.value for x in LedBarMode],
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
value_fn=lambda config: config.led_bar_mode,
|
||||||
|
set_value_fn=lambda client, value: client.set_led_bar_mode(LedBarMode(value)),
|
||||||
|
requires_led_bar=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AirGradientConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up AirGradient select entities based on a config entry."""
|
||||||
|
|
||||||
|
config_coordinator = entry.runtime_data.config
|
||||||
|
measurement_coordinator = entry.runtime_data.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")
|
||||||
|
)
|
||||||
|
or (description.requires_led_bar and "L" in measurement_coordinator.data.model)
|
||||||
|
)
|
||||||
|
|
||||||
|
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.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 . import AirGradientConfigEntry
|
||||||
|
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",
|
||||||
|
native_unit_of_measurement="particles/dL",
|
||||||
|
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: AirGradientConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up AirGradient sensor entities based on a config entry."""
|
||||||
|
|
||||||
|
coordinator = entry.runtime_data.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)
|
81
homeassistant/components/airgradient/strings.json
Normal file
81
homeassistant/components/airgradient/strings.json
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"display_pm_standard": {
|
||||||
|
"name": "Display PM standard",
|
||||||
|
"state": {
|
||||||
|
"ugm3": "µg/m³",
|
||||||
|
"us_aqi": "US AQI"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"led_bar_mode": {
|
||||||
|
"name": "LED bar mode",
|
||||||
|
"state": {
|
||||||
|
"off": "Off",
|
||||||
|
"co2": "Carbon dioxide",
|
||||||
|
"pm": "Particulate matter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sensor": {
|
||||||
|
"total_volatile_organic_component_index": {
|
||||||
|
"name": "VOC index"
|
||||||
|
},
|
||||||
|
"nitrogen_index": {
|
||||||
|
"name": "NOx index"
|
||||||
|
},
|
||||||
|
"pm003_count": {
|
||||||
|
"name": "PM0.3"
|
||||||
|
},
|
||||||
|
"raw_total_volatile_organic_component": {
|
||||||
|
"name": "Raw VOC"
|
||||||
|
},
|
||||||
|
"raw_nitrogen": {
|
||||||
|
"name": "Raw NOx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"no_local_configuration": {
|
||||||
|
"message": "Device should be configured with local configuration to be able to change settings."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,8 +19,10 @@ PLATFORMS = [Platform.SENSOR]
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type AirlyConfigEntry = ConfigEntry[AirlyDataUpdateCoordinator]
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: AirlyConfigEntry) -> bool:
|
||||||
"""Set up Airly as config entry."""
|
"""Set up Airly as config entry."""
|
||||||
api_key = entry.data[CONF_API_KEY]
|
api_key = entry.data[CONF_API_KEY]
|
||||||
latitude = entry.data[CONF_LATITUDE]
|
latitude = entry.data[CONF_LATITUDE]
|
||||||
@ -62,8 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
)
|
)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
entry.runtime_data = coordinator
|
||||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
@ -79,11 +80,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: AirlyConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
CONF_LATITUDE,
|
CONF_LATITUDE,
|
||||||
@ -14,17 +13,16 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import AirlyDataUpdateCoordinator
|
from . import AirlyConfigEntry
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIQUE_ID}
|
TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIQUE_ID}
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
hass: HomeAssistant, config_entry: ConfigEntry
|
hass: HomeAssistant, config_entry: AirlyConfigEntry
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Return diagnostics for a config entry."""
|
"""Return diagnostics for a config entry."""
|
||||||
coordinator: AirlyDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
|
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
|
||||||
|
@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
@ -25,7 +24,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import AirlyDataUpdateCoordinator
|
from . import AirlyConfigEntry, AirlyDataUpdateCoordinator
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_ADVICE,
|
ATTR_ADVICE,
|
||||||
ATTR_API_ADVICE,
|
ATTR_API_ADVICE,
|
||||||
@ -174,12 +173,14 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: AirlyConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up Airly sensor entities based on a config entry."""
|
"""Set up Airly sensor entities based on a config entry."""
|
||||||
name = entry.data[CONF_NAME]
|
name = entry.data[CONF_NAME]
|
||||||
|
|
||||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
(
|
(
|
||||||
|
@ -9,6 +9,7 @@ from airly import Airly
|
|||||||
from homeassistant.components import system_health
|
from homeassistant.components import system_health
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from . import AirlyConfigEntry
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
@ -22,8 +23,10 @@ def async_register(
|
|||||||
|
|
||||||
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
||||||
"""Get info for the info page."""
|
"""Get info for the info page."""
|
||||||
requests_remaining = list(hass.data[DOMAIN].values())[0].airly.requests_remaining
|
config_entry: AirlyConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||||
requests_per_day = list(hass.data[DOMAIN].values())[0].airly.requests_per_day
|
|
||||||
|
requests_remaining = config_entry.runtime_data.airly.requests_remaining
|
||||||
|
requests_per_day = config_entry.runtime_data.airly.requests_per_day
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"can_reach_server": system_health.async_check_can_reach_url(
|
"can_reach_server": system_health.async_check_can_reach_url(
|
||||||
|
@ -21,7 +21,7 @@ from .coordinator import AirNowDataUpdateCoordinator
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
PLATFORMS = [Platform.SENSOR]
|
PLATFORMS = [Platform.SENSOR]
|
||||||
|
|
||||||
AirNowConfigEntry = ConfigEntry[AirNowDataUpdateCoordinator]
|
type AirNowConfigEntry = ConfigEntry[AirNowDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bool:
|
||||||
|
@ -82,7 +82,7 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except InvalidLocation:
|
except InvalidLocation:
|
||||||
errors["base"] = "invalid_location"
|
errors["base"] = "invalid_location"
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
else:
|
else:
|
||||||
|
@ -8,11 +8,13 @@ ATTR_API_CATEGORY = "Category"
|
|||||||
ATTR_API_CAT_LEVEL = "Number"
|
ATTR_API_CAT_LEVEL = "Number"
|
||||||
ATTR_API_CAT_DESCRIPTION = "Name"
|
ATTR_API_CAT_DESCRIPTION = "Name"
|
||||||
ATTR_API_O3 = "O3"
|
ATTR_API_O3 = "O3"
|
||||||
|
ATTR_API_PM10 = "PM10"
|
||||||
ATTR_API_PM25 = "PM2.5"
|
ATTR_API_PM25 = "PM2.5"
|
||||||
ATTR_API_POLLUTANT = "Pollutant"
|
ATTR_API_POLLUTANT = "Pollutant"
|
||||||
ATTR_API_REPORT_DATE = "DateObserved"
|
ATTR_API_REPORT_DATE = "DateObserved"
|
||||||
ATTR_API_REPORT_HOUR = "HourObserved"
|
ATTR_API_REPORT_HOUR = "HourObserved"
|
||||||
ATTR_API_REPORT_TZ = "LocalTimeZone"
|
ATTR_API_REPORT_TZ = "LocalTimeZone"
|
||||||
|
ATTR_API_REPORT_TZINFO = "LocalTimeZoneInfo"
|
||||||
ATTR_API_STATE = "StateCode"
|
ATTR_API_STATE = "StateCode"
|
||||||
ATTR_API_STATION = "ReportingArea"
|
ATTR_API_STATION = "ReportingArea"
|
||||||
ATTR_API_STATION_LATITUDE = "Latitude"
|
ATTR_API_STATION_LATITUDE = "Latitude"
|
||||||
|
@ -12,6 +12,7 @@ from pyairnow.errors import AirNowError
|
|||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_API_AQI,
|
ATTR_API_AQI,
|
||||||
@ -26,6 +27,7 @@ from .const import (
|
|||||||
ATTR_API_REPORT_DATE,
|
ATTR_API_REPORT_DATE,
|
||||||
ATTR_API_REPORT_HOUR,
|
ATTR_API_REPORT_HOUR,
|
||||||
ATTR_API_REPORT_TZ,
|
ATTR_API_REPORT_TZ,
|
||||||
|
ATTR_API_REPORT_TZINFO,
|
||||||
ATTR_API_STATE,
|
ATTR_API_STATE,
|
||||||
ATTR_API_STATION,
|
ATTR_API_STATION,
|
||||||
ATTR_API_STATION_LATITUDE,
|
ATTR_API_STATION_LATITUDE,
|
||||||
@ -96,7 +98,9 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
# Copy Report Details
|
# Copy Report Details
|
||||||
data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE]
|
data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE]
|
||||||
data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR]
|
data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR]
|
||||||
data[ATTR_API_REPORT_TZ] = obv[ATTR_API_REPORT_TZ]
|
data[ATTR_API_REPORT_TZINFO] = await dt_util.async_get_time_zone(
|
||||||
|
obv[ATTR_API_REPORT_TZ]
|
||||||
|
)
|
||||||
|
|
||||||
# Copy Station Details
|
# Copy Station Details
|
||||||
data[ATTR_API_STATE] = obv[ATTR_API_STATE]
|
data[ATTR_API_STATE] = obv[ATTR_API_STATE]
|
||||||
|
@ -4,6 +4,9 @@
|
|||||||
"aqi": {
|
"aqi": {
|
||||||
"default": "mdi:blur"
|
"default": "mdi:blur"
|
||||||
},
|
},
|
||||||
|
"pm10": {
|
||||||
|
"default": "mdi:blur"
|
||||||
|
},
|
||||||
"pm25": {
|
"pm25": {
|
||||||
"default": "mdi:blur"
|
"default": "mdi:blur"
|
||||||
},
|
},
|
||||||
|
@ -23,7 +23,6 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util.dt import get_time_zone
|
|
||||||
|
|
||||||
from . import AirNowConfigEntry, AirNowDataUpdateCoordinator
|
from . import AirNowConfigEntry, AirNowDataUpdateCoordinator
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -31,10 +30,11 @@ from .const import (
|
|||||||
ATTR_API_AQI_DESCRIPTION,
|
ATTR_API_AQI_DESCRIPTION,
|
||||||
ATTR_API_AQI_LEVEL,
|
ATTR_API_AQI_LEVEL,
|
||||||
ATTR_API_O3,
|
ATTR_API_O3,
|
||||||
|
ATTR_API_PM10,
|
||||||
ATTR_API_PM25,
|
ATTR_API_PM25,
|
||||||
ATTR_API_REPORT_DATE,
|
ATTR_API_REPORT_DATE,
|
||||||
ATTR_API_REPORT_HOUR,
|
ATTR_API_REPORT_HOUR,
|
||||||
ATTR_API_REPORT_TZ,
|
ATTR_API_REPORT_TZINFO,
|
||||||
ATTR_API_STATION,
|
ATTR_API_STATION,
|
||||||
ATTR_API_STATION_LATITUDE,
|
ATTR_API_STATION_LATITUDE,
|
||||||
ATTR_API_STATION_LONGITUDE,
|
ATTR_API_STATION_LONGITUDE,
|
||||||
@ -83,10 +83,19 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
|
|||||||
f"{data[ATTR_API_REPORT_DATE]} {data[ATTR_API_REPORT_HOUR]}",
|
f"{data[ATTR_API_REPORT_DATE]} {data[ATTR_API_REPORT_HOUR]}",
|
||||||
"%Y-%m-%d %H",
|
"%Y-%m-%d %H",
|
||||||
)
|
)
|
||||||
.replace(tzinfo=get_time_zone(data[ATTR_API_REPORT_TZ]))
|
.replace(tzinfo=data[ATTR_API_REPORT_TZINFO])
|
||||||
.isoformat(),
|
.isoformat(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
AirNowEntityDescription(
|
||||||
|
key=ATTR_API_PM10,
|
||||||
|
translation_key="pm10",
|
||||||
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.PM10,
|
||||||
|
value_fn=lambda data: data.get(ATTR_API_PM10),
|
||||||
|
extra_state_attributes_fn=None,
|
||||||
|
),
|
||||||
AirNowEntityDescription(
|
AirNowEntityDescription(
|
||||||
key=ATTR_API_PM25,
|
key=ATTR_API_PM25,
|
||||||
translation_key="pm25",
|
translation_key="pm25",
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
"name": "[%key:component::sensor::entity_component::ozone::name%]"
|
"name": "[%key:component::sensor::entity_component::ozone::name%]"
|
||||||
},
|
},
|
||||||
"station": {
|
"station": {
|
||||||
"name": "PM2.5 reporting station",
|
"name": "Reporting station",
|
||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"lat": { "name": "[%key:common::config_flow::data::latitude%]" },
|
"lat": { "name": "[%key:common::config_flow::data::latitude%]" },
|
||||||
"long": { "name": "[%key:common::config_flow::data::longitude%]" }
|
"long": { "name": "[%key:common::config_flow::data::longitude%]" }
|
||||||
|
@ -6,6 +6,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import CONF_CLIP_NEGATIVE, CONF_RETURN_AVERAGE
|
||||||
from .coordinator import AirQCoordinator
|
from .coordinator import AirQCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
@ -16,7 +17,12 @@ AirQConfigEntry = ConfigEntry[AirQCoordinator]
|
|||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirQConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AirQConfigEntry) -> bool:
|
||||||
"""Set up air-Q from a config entry."""
|
"""Set up air-Q from a config entry."""
|
||||||
|
|
||||||
coordinator = AirQCoordinator(hass, entry)
|
coordinator = AirQCoordinator(
|
||||||
|
hass,
|
||||||
|
entry,
|
||||||
|
clip_negative=entry.options.get(CONF_CLIP_NEGATIVE, True),
|
||||||
|
return_average=entry.options.get(CONF_RETURN_AVERAGE, True),
|
||||||
|
)
|
||||||
|
|
||||||
# Query the device for the first time and initialise coordinator.data
|
# Query the device for the first time and initialise coordinator.data
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
@ -24,6 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirQConfigEntry) -> bool
|
|||||||
entry.runtime_data = coordinator
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -31,3 +38,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirQConfigEntry) -> bool
|
|||||||
async def async_unload_entry(hass: HomeAssistant, entry: AirQConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: AirQConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Handle options update."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
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