Compare commits

..

1 Commits

Author SHA1 Message Date
jbouwh
dba2295371 Test for async_get_hass 2023-07-07 15:36:27 +00:00
5201 changed files with 75391 additions and 243908 deletions

View File

@@ -24,13 +24,11 @@ base_platforms: &base_platforms
- homeassistant/components/datetime/**
- homeassistant/components/device_tracker/**
- homeassistant/components/diagnostics/**
- homeassistant/components/event/**
- homeassistant/components/fan/**
- homeassistant/components/geo_location/**
- homeassistant/components/humidifier/**
- homeassistant/components/image/**
- homeassistant/components/image_processing/**
- homeassistant/components/lawn_mower/**
- homeassistant/components/light/**
- homeassistant/components/lock/**
- homeassistant/components/media_player/**
@@ -55,7 +53,6 @@ base_platforms: &base_platforms
components: &components
- homeassistant/components/alexa/**
- homeassistant/components/application_credentials/**
- homeassistant/components/assist_pipeline/**
- homeassistant/components/auth/**
- homeassistant/components/automation/**
- homeassistant/components/backup/**
@@ -88,7 +85,6 @@ components: &components
- homeassistant/components/lovelace/**
- homeassistant/components/media_source/**
- homeassistant/components/mjpeg/**
- homeassistant/components/modbus/**
- homeassistant/components/mqtt/**
- homeassistant/components/network/**
- homeassistant/components/onboarding/**

View File

@@ -29,13 +29,11 @@ omit =
homeassistant/components/adguard/switch.py
homeassistant/components/ads/*
homeassistant/components/aemet/weather_update_coordinator.py
homeassistant/components/aftership/__init__.py
homeassistant/components/aftership/sensor.py
homeassistant/components/aftership/*
homeassistant/components/agent_dvr/alarm_control_panel.py
homeassistant/components/agent_dvr/camera.py
homeassistant/components/agent_dvr/helpers.py
homeassistant/components/airnow/__init__.py
homeassistant/components/airnow/coordinator.py
homeassistant/components/airnow/sensor.py
homeassistant/components/airq/__init__.py
homeassistant/components/airq/coordinator.py
@@ -46,7 +44,6 @@ omit =
homeassistant/components/airthings_ble/sensor.py
homeassistant/components/airtouch4/__init__.py
homeassistant/components/airtouch4/climate.py
homeassistant/components/airtouch4/coordinator.py
homeassistant/components/airvisual/__init__.py
homeassistant/components/airvisual/sensor.py
homeassistant/components/airvisual_pro/__init__.py
@@ -60,7 +57,6 @@ omit =
homeassistant/components/ambiclimate/climate.py
homeassistant/components/ambient_station/__init__.py
homeassistant/components/ambient_station/binary_sensor.py
homeassistant/components/ambient_station/entity.py
homeassistant/components/ambient_station/sensor.py
homeassistant/components/amcrest/*
homeassistant/components/ampio/*
@@ -86,7 +82,6 @@ omit =
homeassistant/components/arwn/sensor.py
homeassistant/components/aseko_pool_live/__init__.py
homeassistant/components/aseko_pool_live/binary_sensor.py
homeassistant/components/aseko_pool_live/coordinator.py
homeassistant/components/aseko_pool_live/entity.py
homeassistant/components/aseko_pool_live/sensor.py
homeassistant/components/asterisk_cdr/mailbox.py
@@ -103,7 +98,6 @@ omit =
homeassistant/components/azure_devops/__init__.py
homeassistant/components/azure_devops/sensor.py
homeassistant/components/azure_service_bus/*
homeassistant/components/awair/coordinator.py
homeassistant/components/baf/__init__.py
homeassistant/components/baf/climate.py
homeassistant/components/baf/entity.py
@@ -173,18 +167,12 @@ omit =
homeassistant/components/cmus/media_player.py
homeassistant/components/coinbase/sensor.py
homeassistant/components/comed_hourly_pricing/sensor.py
homeassistant/components/comelit/__init__.py
homeassistant/components/comelit/const.py
homeassistant/components/comelit/cover.py
homeassistant/components/comelit/coordinator.py
homeassistant/components/comelit/light.py
homeassistant/components/comfoconnect/fan.py
homeassistant/components/concord232/alarm_control_panel.py
homeassistant/components/concord232/binary_sensor.py
homeassistant/components/control4/__init__.py
homeassistant/components/control4/director_utils.py
homeassistant/components/control4/light.py
homeassistant/components/coolmaster/coordinator.py
homeassistant/components/cppm_tracker/device_tracker.py
homeassistant/components/crownstone/__init__.py
homeassistant/components/crownstone/devices.py
@@ -223,12 +211,10 @@ omit =
homeassistant/components/dominos/*
homeassistant/components/doods/*
homeassistant/components/doorbird/__init__.py
homeassistant/components/doorbird/camera.py
homeassistant/components/doorbird/button.py
homeassistant/components/doorbird/device.py
homeassistant/components/doorbird/camera.py
homeassistant/components/doorbird/entity.py
homeassistant/components/doorbird/util.py
homeassistant/components/doorbird/view.py
homeassistant/components/dormakaba_dkey/__init__.py
homeassistant/components/dormakaba_dkey/binary_sensor.py
homeassistant/components/dormakaba_dkey/entity.py
@@ -243,13 +229,6 @@ omit =
homeassistant/components/dublin_bus_transport/sensor.py
homeassistant/components/dunehd/__init__.py
homeassistant/components/dunehd/media_player.py
homeassistant/components/duotecno/__init__.py
homeassistant/components/duotecno/entity.py
homeassistant/components/duotecno/switch.py
homeassistant/components/duotecno/cover.py
homeassistant/components/duotecno/light.py
homeassistant/components/duotecno/climate.py
homeassistant/components/duotecno/binary_sensor.py
homeassistant/components/dwd_weather_warnings/const.py
homeassistant/components/dwd_weather_warnings/coordinator.py
homeassistant/components/dwd_weather_warnings/sensor.py
@@ -263,12 +242,6 @@ omit =
homeassistant/components/ecobee/notify.py
homeassistant/components/ecobee/sensor.py
homeassistant/components/ecobee/weather.py
homeassistant/components/ecoforest/__init__.py
homeassistant/components/ecoforest/coordinator.py
homeassistant/components/ecoforest/entity.py
homeassistant/components/ecoforest/number.py
homeassistant/components/ecoforest/sensor.py
homeassistant/components/ecoforest/switch.py
homeassistant/components/econet/__init__.py
homeassistant/components/econet/binary_sensor.py
homeassistant/components/econet/climate.py
@@ -287,11 +260,6 @@ omit =
homeassistant/components/eight_sleep/__init__.py
homeassistant/components/eight_sleep/binary_sensor.py
homeassistant/components/eight_sleep/sensor.py
homeassistant/components/electric_kiwi/__init__.py
homeassistant/components/electric_kiwi/api.py
homeassistant/components/electric_kiwi/oauth2.py
homeassistant/components/electric_kiwi/coordinator.py
homeassistant/components/electric_kiwi/select.py
homeassistant/components/eliqonline/sensor.py
homeassistant/components/elkm1/__init__.py
homeassistant/components/elkm1/alarm_control_panel.py
@@ -304,8 +272,7 @@ omit =
homeassistant/components/elmax/alarm_control_panel.py
homeassistant/components/elmax/binary_sensor.py
homeassistant/components/elmax/common.py
homeassistant/components/elmax/const.py
homeassistant/components/elmax/cover.py
homeassistant/components/elmax/binary_sensor.py
homeassistant/components/elmax/switch.py
homeassistant/components/elv/*
homeassistant/components/emby/media_player.py
@@ -322,13 +289,7 @@ omit =
homeassistant/components/enocean/sensor.py
homeassistant/components/enocean/switch.py
homeassistant/components/enphase_envoy/__init__.py
homeassistant/components/enphase_envoy/binary_sensor.py
homeassistant/components/enphase_envoy/coordinator.py
homeassistant/components/enphase_envoy/entity.py
homeassistant/components/enphase_envoy/number.py
homeassistant/components/enphase_envoy/select.py
homeassistant/components/enphase_envoy/sensor.py
homeassistant/components/enphase_envoy/switch.py
homeassistant/components/entur_public_transport/*
homeassistant/components/environment_canada/__init__.py
homeassistant/components/environment_canada/camera.py
@@ -343,8 +304,10 @@ omit =
homeassistant/components/escea/__init__.py
homeassistant/components/escea/climate.py
homeassistant/components/escea/discovery.py
homeassistant/components/esphome/__init__.py
homeassistant/components/esphome/bluetooth/*
homeassistant/components/esphome/manager.py
homeassistant/components/esphome/domain_data.py
homeassistant/components/esphome/entry_data.py
homeassistant/components/etherscan/sensor.py
homeassistant/components/eufy/*
homeassistant/components/eufylife_ble/__init__.py
@@ -352,23 +315,17 @@ omit =
homeassistant/components/everlights/light.py
homeassistant/components/evohome/*
homeassistant/components/ezviz/__init__.py
homeassistant/components/ezviz/alarm_control_panel.py
homeassistant/components/ezviz/binary_sensor.py
homeassistant/components/ezviz/button.py
homeassistant/components/ezviz/camera.py
homeassistant/components/ezviz/image.py
homeassistant/components/ezviz/light.py
homeassistant/components/ezviz/coordinator.py
homeassistant/components/ezviz/number.py
homeassistant/components/ezviz/entity.py
homeassistant/components/ezviz/select.py
homeassistant/components/ezviz/sensor.py
homeassistant/components/ezviz/siren.py
homeassistant/components/ezviz/switch.py
homeassistant/components/ezviz/update.py
homeassistant/components/faa_delays/__init__.py
homeassistant/components/faa_delays/binary_sensor.py
homeassistant/components/faa_delays/coordinator.py
homeassistant/components/familyhub/camera.py
homeassistant/components/fastdotcom/*
homeassistant/components/ffmpeg/camera.py
@@ -393,6 +350,7 @@ omit =
homeassistant/components/firmata/pin.py
homeassistant/components/firmata/sensor.py
homeassistant/components/firmata/switch.py
homeassistant/components/fitbit/*
homeassistant/components/fivem/__init__.py
homeassistant/components/fivem/binary_sensor.py
homeassistant/components/fivem/coordinator.py
@@ -429,6 +387,7 @@ omit =
homeassistant/components/freebox/device_tracker.py
homeassistant/components/freebox/home_base.py
homeassistant/components/freebox/router.py
homeassistant/components/freebox/sensor.py
homeassistant/components/freebox/switch.py
homeassistant/components/fritz/common.py
homeassistant/components/fritz/device_tracker.py
@@ -444,7 +403,6 @@ omit =
homeassistant/components/garadget/cover.py
homeassistant/components/garages_amsterdam/__init__.py
homeassistant/components/garages_amsterdam/binary_sensor.py
homeassistant/components/garages_amsterdam/entity.py
homeassistant/components/garages_amsterdam/sensor.py
homeassistant/components/gc100/*
homeassistant/components/geniushub/*
@@ -541,12 +499,7 @@ omit =
homeassistant/components/hvv_departures/__init__.py
homeassistant/components/hvv_departures/binary_sensor.py
homeassistant/components/hvv_departures/sensor.py
homeassistant/components/hydrawise/__init__.py
homeassistant/components/hydrawise/binary_sensor.py
homeassistant/components/hydrawise/const.py
homeassistant/components/hydrawise/coordinator.py
homeassistant/components/hydrawise/sensor.py
homeassistant/components/hydrawise/switch.py
homeassistant/components/hydrawise/*
homeassistant/components/ialarm/alarm_control_panel.py
homeassistant/components/iammeter/sensor.py
homeassistant/components/iaqualink/binary_sensor.py
@@ -640,7 +593,6 @@ omit =
homeassistant/components/keymitt_ble/entity.py
homeassistant/components/keymitt_ble/switch.py
homeassistant/components/keymitt_ble/coordinator.py
homeassistant/components/kitchen_sink/weather.py
homeassistant/components/kiwi/lock.py
homeassistant/components/kodi/__init__.py
homeassistant/components/kodi/browse_media.py
@@ -672,7 +624,6 @@ omit =
homeassistant/components/lg_soundbar/__init__.py
homeassistant/components/lg_soundbar/media_player.py
homeassistant/components/life360/__init__.py
homeassistant/components/life360/button.py
homeassistant/components/life360/coordinator.py
homeassistant/components/life360/device_tracker.py
homeassistant/components/lightwave/*
@@ -722,13 +673,11 @@ omit =
homeassistant/components/mailgun/notify.py
homeassistant/components/map/*
homeassistant/components/mastodon/notify.py
homeassistant/components/matrix/__init__.py
homeassistant/components/matrix/notify.py
homeassistant/components/matrix/*
homeassistant/components/matter/__init__.py
homeassistant/components/meater/__init__.py
homeassistant/components/meater/sensor.py
homeassistant/components/medcom_ble/__init__.py
homeassistant/components/medcom_ble/sensor.py
homeassistant/components/media_extractor/*
homeassistant/components/mediaroom/media_player.py
homeassistant/components/melcloud/__init__.py
homeassistant/components/melcloud/climate.py
@@ -746,16 +695,16 @@ omit =
homeassistant/components/meteoclimatic/__init__.py
homeassistant/components/meteoclimatic/sensor.py
homeassistant/components/meteoclimatic/weather.py
homeassistant/components/metoffice/sensor.py
homeassistant/components/metoffice/weather.py
homeassistant/components/microsoft/tts.py
homeassistant/components/miflora/sensor.py
homeassistant/components/mikrotik/hub.py
homeassistant/components/mill/climate.py
homeassistant/components/mill/sensor.py
homeassistant/components/minecraft_server/__init__.py
homeassistant/components/minecraft_server/binary_sensor.py
homeassistant/components/minecraft_server/coordinator.py
homeassistant/components/minecraft_server/entity.py
homeassistant/components/minecraft_server/sensor.py
homeassistant/components/minio/minio_helper.py
homeassistant/components/mitemp_bt/sensor.py
homeassistant/components/mjpeg/camera.py
homeassistant/components/mjpeg/util.py
homeassistant/components/mochad/__init__.py
@@ -768,9 +717,7 @@ omit =
homeassistant/components/moehlenhoff_alpha2/climate.py
homeassistant/components/moehlenhoff_alpha2/sensor.py
homeassistant/components/motion_blinds/__init__.py
homeassistant/components/motion_blinds/coordinator.py
homeassistant/components/motion_blinds/cover.py
homeassistant/components/motion_blinds/entity.py
homeassistant/components/motion_blinds/sensor.py
homeassistant/components/mpd/media_player.py
homeassistant/components/mqtt_room/sensor.py
@@ -803,18 +750,16 @@ omit =
homeassistant/components/neato/__init__.py
homeassistant/components/neato/api.py
homeassistant/components/neato/camera.py
homeassistant/components/neato/entity.py
homeassistant/components/neato/hub.py
homeassistant/components/neato/sensor.py
homeassistant/components/neato/switch.py
homeassistant/components/neato/vacuum.py
homeassistant/components/neato/button.py
homeassistant/components/nederlandse_spoorwegen/sensor.py
homeassistant/components/nest/legacy/*
homeassistant/components/netdata/sensor.py
homeassistant/components/netgear/__init__.py
homeassistant/components/netgear/button.py
homeassistant/components/netgear/device_tracker.py
homeassistant/components/netgear/entity.py
homeassistant/components/netgear/router.py
homeassistant/components/netgear/sensor.py
homeassistant/components/netgear/switch.py
@@ -867,7 +812,6 @@ omit =
homeassistant/components/obihai/connectivity.py
homeassistant/components/obihai/sensor.py
homeassistant/components/octoprint/__init__.py
homeassistant/components/octoprint/coordinator.py
homeassistant/components/oem/climate.py
homeassistant/components/ohmconnect/sensor.py
homeassistant/components/ombi/*
@@ -898,11 +842,11 @@ omit =
homeassistant/components/opengarage/cover.py
homeassistant/components/opengarage/entity.py
homeassistant/components/opengarage/sensor.py
homeassistant/components/openhardwaremonitor/sensor.py
homeassistant/components/openhome/__init__.py
homeassistant/components/openhome/const.py
homeassistant/components/openhome/media_player.py
homeassistant/components/opensensemap/air_quality.py
homeassistant/components/opensky/sensor.py
homeassistant/components/opentherm_gw/__init__.py
homeassistant/components/opentherm_gw/binary_sensor.py
homeassistant/components/opentherm_gw/climate.py
@@ -980,8 +924,6 @@ omit =
homeassistant/components/point/sensor.py
homeassistant/components/poolsense/__init__.py
homeassistant/components/poolsense/binary_sensor.py
homeassistant/components/poolsense/coordinator.py
homeassistant/components/poolsense/entity.py
homeassistant/components/poolsense/sensor.py
homeassistant/components/powerwall/__init__.py
homeassistant/components/progettihwsw/__init__.py
@@ -1032,13 +974,8 @@ omit =
homeassistant/components/rainmachine/util.py
homeassistant/components/renson/__init__.py
homeassistant/components/renson/const.py
homeassistant/components/renson/coordinator.py
homeassistant/components/renson/entity.py
homeassistant/components/renson/sensor.py
homeassistant/components/renson/button.py
homeassistant/components/renson/fan.py
homeassistant/components/renson/binary_sensor.py
homeassistant/components/renson/number.py
homeassistant/components/raspyrfm/*
homeassistant/components/recollect_waste/sensor.py
homeassistant/components/recorder/repack.py
@@ -1055,7 +992,6 @@ omit =
homeassistant/components/reolink/light.py
homeassistant/components/reolink/number.py
homeassistant/components/reolink/select.py
homeassistant/components/reolink/sensor.py
homeassistant/components/reolink/siren.py
homeassistant/components/reolink/switch.py
homeassistant/components/reolink/update.py
@@ -1099,10 +1035,9 @@ omit =
homeassistant/components/saj/sensor.py
homeassistant/components/satel_integra/*
homeassistant/components/schluter/*
homeassistant/components/screenlogic/__init__.py
homeassistant/components/screenlogic/binary_sensor.py
homeassistant/components/screenlogic/climate.py
homeassistant/components/screenlogic/coordinator.py
homeassistant/components/screenlogic/const.py
homeassistant/components/screenlogic/entity.py
homeassistant/components/screenlogic/light.py
homeassistant/components/screenlogic/number.py
@@ -1166,7 +1101,6 @@ omit =
homeassistant/components/smarty/*
homeassistant/components/sms/__init__.py
homeassistant/components/sms/const.py
homeassistant/components/sms/coordinator.py
homeassistant/components/sms/gateway.py
homeassistant/components/sms/notify.py
homeassistant/components/sms/sensor.py
@@ -1183,7 +1117,6 @@ omit =
homeassistant/components/solaredge_local/sensor.py
homeassistant/components/solarlog/__init__.py
homeassistant/components/solarlog/sensor.py
homeassistant/components/solarlog/coordinator.py
homeassistant/components/solax/__init__.py
homeassistant/components/solax/sensor.py
homeassistant/components/soma/__init__.py
@@ -1220,13 +1153,7 @@ omit =
homeassistant/components/squeezebox/__init__.py
homeassistant/components/squeezebox/browse_media.py
homeassistant/components/squeezebox/media_player.py
homeassistant/components/starlink/__init__.py
homeassistant/components/starlink/binary_sensor.py
homeassistant/components/starlink/button.py
homeassistant/components/starlink/coordinator.py
homeassistant/components/starlink/device_tracker.py
homeassistant/components/starlink/sensor.py
homeassistant/components/starlink/switch.py
homeassistant/components/starline/__init__.py
homeassistant/components/starline/account.py
homeassistant/components/starline/binary_sensor.py
@@ -1276,9 +1203,6 @@ omit =
homeassistant/components/switchbot/sensor.py
homeassistant/components/switchbot/switch.py
homeassistant/components/switchbot/lock.py
homeassistant/components/switchbot_cloud/coordinator.py
homeassistant/components/switchbot_cloud/entity.py
homeassistant/components/switchbot_cloud/switch.py
homeassistant/components/switchmate/switch.py
homeassistant/components/syncthing/__init__.py
homeassistant/components/syncthing/sensor.py
@@ -1301,7 +1225,6 @@ omit =
homeassistant/components/system_bridge/__init__.py
homeassistant/components/system_bridge/binary_sensor.py
homeassistant/components/system_bridge/coordinator.py
homeassistant/components/system_bridge/notify.py
homeassistant/components/system_bridge/sensor.py
homeassistant/components/systemmonitor/sensor.py
homeassistant/components/tado/__init__.py
@@ -1313,8 +1236,6 @@ omit =
homeassistant/components/tank_utility/sensor.py
homeassistant/components/tankerkoenig/__init__.py
homeassistant/components/tankerkoenig/binary_sensor.py
homeassistant/components/tankerkoenig/coordinator.py
homeassistant/components/tankerkoenig/entity.py
homeassistant/components/tankerkoenig/sensor.py
homeassistant/components/tapsaff/binary_sensor.py
homeassistant/components/tautulli/__init__.py
@@ -1376,6 +1297,9 @@ omit =
homeassistant/components/tplink_omada/__init__.py
homeassistant/components/tplink_omada/binary_sensor.py
homeassistant/components/tplink_omada/controller.py
homeassistant/components/tplink_omada/coordinator.py
homeassistant/components/tplink_omada/entity.py
homeassistant/components/tplink_omada/switch.py
homeassistant/components/tplink_omada/update.py
homeassistant/components/traccar/device_tracker.py
homeassistant/components/tractive/__init__.py
@@ -1393,13 +1317,10 @@ omit =
homeassistant/components/tradfri/sensor.py
homeassistant/components/tradfri/switch.py
homeassistant/components/trafikverket_train/__init__.py
homeassistant/components/trafikverket_train/coordinator.py
homeassistant/components/trafikverket_train/sensor.py
homeassistant/components/trafikverket_train/util.py
homeassistant/components/trafikverket_weatherstation/__init__.py
homeassistant/components/trafikverket_weatherstation/coordinator.py
homeassistant/components/trafikverket_weatherstation/sensor.py
homeassistant/components/transmission/__init__.py
homeassistant/components/transmission/sensor.py
homeassistant/components/transmission/switch.py
homeassistant/components/travisci/sensor.py
@@ -1482,12 +1403,6 @@ omit =
homeassistant/components/vlc/media_player.py
homeassistant/components/vlc_telnet/__init__.py
homeassistant/components/vlc_telnet/media_player.py
homeassistant/components/vodafone_station/__init__.py
homeassistant/components/vodafone_station/button.py
homeassistant/components/vodafone_station/const.py
homeassistant/components/vodafone_station/coordinator.py
homeassistant/components/vodafone_station/device_tracker.py
homeassistant/components/vodafone_station/sensor.py
homeassistant/components/volkszaehler/sensor.py
homeassistant/components/volumio/__init__.py
homeassistant/components/volumio/browse_media.py
@@ -1508,15 +1423,11 @@ omit =
homeassistant/components/watson_tts/tts.py
homeassistant/components/watttime/__init__.py
homeassistant/components/watttime/sensor.py
homeassistant/components/weatherflow/__init__.py
homeassistant/components/weatherflow/const.py
homeassistant/components/weatherflow/sensor.py
homeassistant/components/wiffi/__init__.py
homeassistant/components/wiffi/binary_sensor.py
homeassistant/components/wiffi/sensor.py
homeassistant/components/wiffi/wiffi_strings.py
homeassistant/components/wirelesstag/*
homeassistant/components/withings/api.py
homeassistant/components/wolflink/__init__.py
homeassistant/components/wolflink/sensor.py
homeassistant/components/worldtidesinfo/sensor.py
@@ -1561,6 +1472,7 @@ omit =
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/coordinator.py
homeassistant/components/yale_smart_alarm/entity.py
homeassistant/components/yale_smart_alarm/lock.py
homeassistant/components/yalexs_ble/__init__.py
@@ -1575,9 +1487,6 @@ omit =
homeassistant/components/yamaha_musiccast/select.py
homeassistant/components/yamaha_musiccast/switch.py
homeassistant/components/yandex_transport/sensor.py
homeassistant/components/yardian/__init__.py
homeassistant/components/yardian/coordinator.py
homeassistant/components/yardian/switch.py
homeassistant/components/yeelightsunflower/light.py
homeassistant/components/yi/camera.py
homeassistant/components/yolink/__init__.py

View File

@@ -7,46 +7,42 @@
"containerEnv": { "DEVCONTAINER": "1" },
"appPort": ["8123:8123"],
"runArgs": ["-e", "GIT_EDITOR=code --wait"],
"customizations": {
"vscode": {
"extensions": [
"ms-python.vscode-pylance",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"esbenp.prettier-vscode",
"GitHub.vscode-pull-request-github"
],
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.blackPath": "/usr/local/bin/black",
"python.linting.pycodestylePath": "/usr/local/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/bin/pydocstyle",
"python.linting.mypyPath": "/usr/local/bin/mypy",
"python.linting.pylintPath": "/usr/local/bin/pylint",
"python.formatting.provider": "black",
"python.testing.pytestArgs": ["--no-cov"],
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/usr/bin/zsh"
}
},
"terminal.integrated.defaultProfile.linux": "zsh",
"yaml.customTags": [
"!input scalar",
"!secret scalar",
"!include_dir_named scalar",
"!include_dir_list scalar",
"!include_dir_merge_list scalar",
"!include_dir_merge_named scalar"
]
"extensions": [
"ms-python.vscode-pylance",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml",
"esbenp.prettier-vscode",
"GitHub.vscode-pull-request-github"
],
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.blackPath": "/usr/local/bin/black",
"python.linting.pycodestylePath": "/usr/local/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/bin/pydocstyle",
"python.linting.mypyPath": "/usr/local/bin/mypy",
"python.linting.pylintPath": "/usr/local/bin/pylint",
"python.formatting.provider": "black",
"python.testing.pytestArgs": ["--no-cov"],
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/usr/bin/zsh"
}
}
},
"terminal.integrated.defaultProfile.linux": "zsh",
"yaml.customTags": [
"!input scalar",
"!secret scalar",
"!include_dir_named scalar",
"!include_dir_list scalar",
"!include_dir_merge_list scalar",
"!include_dir_merge_named scalar"
]
}
}

View File

@@ -24,12 +24,12 @@ jobs:
publish: ${{ steps.version.outputs.publish }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
with:
fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -56,10 +56,10 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -98,7 +98,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
@@ -124,7 +124,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -190,14 +190,14 @@ jobs:
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
uses: docker/login-action@v2.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2023.09.0
uses: home-assistant/builder@2023.06.1
with:
args: |
$BUILD_ARGS \
@@ -205,6 +205,8 @@ jobs:
--cosign \
--target /data \
--generic ${{ needs.init.outputs.version }}
env:
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
- name: Archive translations
shell: bash
@@ -249,10 +251,9 @@ jobs:
- raspberrypi4-64
- tinker
- yellow
- green
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set build additional args
run: |
@@ -266,20 +267,22 @@ jobs:
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
uses: docker/login-action@v2.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2023.09.0
uses: home-assistant/builder@2023.06.1
with:
args: |
$BUILD_ARGS \
--target /data/machine \
--cosign \
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
env:
CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
publish_ha:
name: Publish version files
@@ -289,7 +292,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Initialize git
uses: home-assistant/actions/helpers/git-init@master
@@ -327,21 +330,21 @@ jobs:
id-token: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Install Cosign
uses: sigstore/cosign-installer@v3.1.2
uses: sigstore/cosign-installer@v3.1.1
with:
cosign-release: "v2.0.2"
- name: Login to DockerHub
uses: docker/login-action@v3.0.0
uses: docker/login-action@v2.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
uses: docker/login-action@v2.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}

View File

@@ -19,10 +19,6 @@ on:
description: "Skip pytest"
default: false
type: boolean
skip-coverage:
description: "Skip coverage"
default: false
type: boolean
pylint-only:
description: "Only run pylint"
default: false
@@ -35,11 +31,10 @@ on:
env:
CACHE_VERSION: 5
PIP_CACHE_VERSION: 4
MYPY_CACHE_VERSION: 5
BLACK_CACHE_VERSION: 1
HA_SHORT_VERSION: "2023.10"
DEFAULT_PYTHON: "3.11"
ALL_PYTHON_VERSIONS: "['3.11']"
MYPY_CACHE_VERSION: 4
HA_SHORT_VERSION: 2023.8
DEFAULT_PYTHON: "3.10"
ALL_PYTHON_VERSIONS: "['3.10', '3.11']"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support
@@ -56,7 +51,6 @@ env:
POSTGRESQL_VERSIONS: "['postgres:12.14','postgres:15.2']"
PRE_COMMIT_CACHE: ~/.cache/pre-commit
PIP_CACHE: /tmp/pip-cache
BLACK_CACHE: /tmp/black-cache
SQLALCHEMY_WARN_20: 1
PYTHONASYNCIODEBUG: 1
HASS_CI: 1
@@ -85,11 +79,10 @@ jobs:
test_groups: ${{ steps.info.outputs.test_groups }}
tests_glob: ${{ steps.info.outputs.tests_glob }}
tests: ${{ steps.info.outputs.tests }}
skip_coverage: ${{ steps.info.outputs.skip_coverage }}
runs-on: ubuntu-22.04
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Generate partial Python venv restore key
id: generate_python_cache_key
run: >-
@@ -134,7 +127,6 @@ jobs:
test_group_count=10
tests="[]"
tests_glob=""
skip_coverage=""
if [[ "${{ steps.integrations.outputs.changes }}" != "[]" ]];
then
@@ -184,12 +176,6 @@ jobs:
test_full_suite="true"
fi
if [[ "${{ github.event.inputs.skip-coverage }}" == "true" ]] \
|| [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-skip-coverage') }}" == "true" ]];
then
skip_coverage="true"
fi
# Output & sent to GitHub Actions
echo "mariadb_groups: ${mariadb_groups}"
echo "mariadb_groups=${mariadb_groups}" >> $GITHUB_OUTPUT
@@ -209,8 +195,6 @@ jobs:
echo "tests=${tests}" >> $GITHUB_OUTPUT
echo "tests_glob: ${tests_glob}"
echo "tests_glob=${tests_glob}" >> $GITHUB_OUTPUT
echo "skip_coverage: ${skip_coverage}"
echo "skip_coverage=${skip_coverage}" >> $GITHUB_OUTPUT
pre-commit:
name: Prepare pre-commit base
@@ -222,16 +206,16 @@ jobs:
- info
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@v3.3.2
uses: actions/cache@v3.3.1
with:
path: venv
key: >-
@@ -246,7 +230,7 @@ jobs:
pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache@v3.3.2
uses: actions/cache@v3.3.1
with:
path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true
@@ -267,23 +251,16 @@ jobs:
- pre-commit
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Generate partial black restore key
id: generate-black-key
run: |
black_version=$(cat requirements_test_pre_commit.txt | grep black | cut -d '=' -f 3)
echo "version=$black_version" >> $GITHUB_OUTPUT
echo "key=black-${{ env.BLACK_CACHE_VERSION }}-$black_version-${{
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: venv
fail-on-cache-miss: true
@@ -292,36 +269,21 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Restore black cache
uses: actions/cache@v3.3.2
with:
path: ${{ env.BLACK_CACHE }}
key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-black-key.outputs.key }}
restore-keys: |
${{ runner.os }}-${{ steps.python.outputs.python-version }}-black-${{
env.BLACK_CACHE_VERSION }}-${{ steps.generate-black-key.outputs.version }}-${{
env.HA_SHORT_VERSION }}-
- name: Run black (fully)
if: needs.info.outputs.test_full_suite == 'true'
env:
BLACK_CACHE_DIR: ${{ env.BLACK_CACHE }}
run: |
. venv/bin/activate
pre-commit run --hook-stage manual black --all-files --show-diff-on-failure
- name: Run black (partially)
if: needs.info.outputs.test_full_suite == 'false'
shell: bash
env:
BLACK_CACHE_DIR: ${{ env.BLACK_CACHE }}
run: |
. venv/bin/activate
shopt -s globstar
@@ -335,16 +297,16 @@ jobs:
- pre-commit
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: venv
fail-on-cache-miss: true
@@ -353,7 +315,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -384,16 +346,16 @@ jobs:
- pre-commit
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: venv
fail-on-cache-miss: true
@@ -402,7 +364,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -478,10 +440,10 @@ jobs:
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -492,7 +454,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@v3.3.2
uses: actions/cache@v3.3.1
with:
path: venv
lookup-only: true
@@ -501,7 +463,7 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore pip wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@v3.3.2
uses: actions/cache@v3.3.1
with:
path: ${{ env.PIP_CACHE }}
key: >-
@@ -530,9 +492,9 @@ jobs:
python -m venv venv
. venv/bin/activate
python --version
PIP_CACHE_DIR=$PIP_CACHE pip install -U "pip>=21.3.1" setuptools wheel
PIP_CACHE_DIR=$PIP_CACHE pip install -r requirements_all.txt
PIP_CACHE_DIR=$PIP_CACHE pip install -r requirements_test.txt
pip install --cache-dir=$PIP_CACHE -U "pip>=21.3.1,<23.2" setuptools wheel
pip install --cache-dir=$PIP_CACHE -r requirements_all.txt
pip install --cache-dir=$PIP_CACHE -r requirements_test.txt
pip install -e . --config-settings editable_mode=compat
hassfest:
@@ -546,16 +508,16 @@ jobs:
- base
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
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@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: venv
fail-on-cache-miss: true
@@ -578,16 +540,16 @@ jobs:
- base
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: venv
fail-on-cache-miss: true
@@ -611,16 +573,16 @@ jobs:
- base
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
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@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: venv
fail-on-cache-miss: true
@@ -655,10 +617,10 @@ jobs:
- base
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -671,7 +633,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: venv
fail-on-cache-miss: true
@@ -679,7 +641,7 @@ jobs:
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@v3.3.2
uses: actions/cache@v3.3.1
with:
path: .mypy_cache
key: >-
@@ -737,16 +699,16 @@ jobs:
bluez \
ffmpeg
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: venv
fail-on-cache-miss: true
@@ -772,19 +734,9 @@ jobs:
- name: Run pytest (fully)
if: needs.info.outputs.test_full_suite == 'true'
timeout-minutes: 60
id: pytest-full
env:
PYTHONDONTWRITEBYTECODE: 1
run: |
. venv/bin/activate
python --version
set -o pipefail
cov_params=()
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant")
cov_params+=(--cov-report=xml)
fi
python3 -X dev -m pytest \
-qq \
--timeout=9 \
@@ -793,54 +745,37 @@ jobs:
--dist=loadfile \
--test-group-count ${{ needs.info.outputs.test_group_count }} \
--test-group=${{ matrix.group }} \
${cov_params[@]} \
--cov="homeassistant" \
--cov-report=xml \
-o console_output_style=count \
-p no:sugar \
tests \
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
tests
- name: Run pytest (partially)
if: needs.info.outputs.test_full_suite == 'false'
timeout-minutes: 10
id: pytest-partial
shell: bash
env:
PYTHONDONTWRITEBYTECODE: 1
run: |
. venv/bin/activate
python --version
set -o pipefail
if [[ ! -f "tests/components/${{ matrix.group }}/__init__.py" ]]; then
echo "::error:: missing file tests/components/${{ matrix.group }}/__init__.py"
exit 1
fi
cov_params=()
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant.components.${{ matrix.group }}")
cov_params+=(--cov-report=xml)
cov_params+=(--cov-report=term-missing)
fi
python3 -X dev -m pytest \
-qq \
--timeout=9 \
-n auto \
${cov_params[@]} \
--cov="homeassistant.components.${{ matrix.group }}" \
--cov-report=xml \
--cov-report=term-missing \
-o console_output_style=count \
--durations=0 \
--durations-min=1 \
-p no:sugar \
tests/components/${{ matrix.group }} \
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && (steps.pytest-full.conclusion == 'failure' || steps.pytest-partial.conclusion == 'failure')
uses: actions/upload-artifact@v3.1.2
with:
name: pytest-${{ github.run_number }}
path: pytest-*.txt
tests/components/${{ matrix.group }}
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v3.1.2
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
@@ -889,16 +824,16 @@ jobs:
ffmpeg \
libmariadb-dev-compat
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: venv
fail-on-cache-miss: true
@@ -927,27 +862,18 @@ jobs:
python3 -m script.translations develop --all
- name: Run pytest (partially)
timeout-minutes: 20
id: pytest-partial
shell: bash
env:
PYTHONDONTWRITEBYTECODE: 1
run: |
. venv/bin/activate
python --version
set -o pipefail
mariadb=$(echo "${{ matrix.mariadb-group }}" | sed "s/:/-/g")
cov_params=()
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant.components.recorder")
cov_params+=(--cov-report=xml)
cov_params+=(--cov-report=term-missing)
fi
python3 -X dev -m pytest \
-qq \
--timeout=20 \
-n 1 \
${cov_params[@]} \
--cov="homeassistant.components.recorder" \
--cov-report=xml \
--cov-report=term-missing \
-o console_output_style=count \
--durations=10 \
-p no:sugar \
@@ -955,16 +881,8 @@ jobs:
tests/components/history \
tests/components/logbook \
tests/components/recorder \
tests/components/sensor \
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v3.1.2
with:
name: pytest-${{ github.run_number }}
path: pytest-*.txt
tests/components/sensor
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v3.1.2
with:
name: coverage-${{ matrix.python-version }}-mariadb
@@ -1013,16 +931,16 @@ jobs:
ffmpeg \
postgresql-server-dev-14
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
with:
python-version: ${{ matrix.python-version }}
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v3.3.2
uses: actions/cache/restore@v3.3.1
with:
path: venv
fail-on-cache-miss: true
@@ -1051,27 +969,18 @@ jobs:
python3 -m script.translations develop --all
- name: Run pytest (partially)
timeout-minutes: 20
id: pytest-partial
shell: bash
env:
PYTHONDONTWRITEBYTECODE: 1
run: |
. venv/bin/activate
python --version
set -o pipefail
postgresql=$(echo "${{ matrix.postgresql-group }}" | sed "s/:/-/g")
cov_params=()
if [[ "${{ needs.info.outputs.skip_coverage }}" != "true" ]]; then
cov_params+=(--cov="homeassistant.components.recorder")
cov_params+=(--cov-report=xml)
cov_params+=(--cov-report=term-missing)
fi
python3 -X dev -m pytest \
-qq \
--timeout=9 \
-n 1 \
${cov_params[@]} \
--cov="homeassistant.components.recorder" \
--cov-report=xml \
--cov-report=term-missing \
-o console_output_style=count \
--durations=0 \
--durations-min=10 \
@@ -1080,16 +989,8 @@ jobs:
tests/components/history \
tests/components/logbook \
tests/components/recorder \
tests/components/sensor \
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v3.1.2
with:
name: pytest-${{ github.run_number }}
path: pytest-*.txt
tests/components/sensor
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v3.1.0
with:
name: coverage-${{ matrix.python-version }}-postgresql
@@ -1100,7 +1001,6 @@ jobs:
coverage:
name: Upload test coverage to Codecov
if: needs.info.outputs.skip_coverage != 'true'
runs-on: ubuntu-22.04
needs:
- info
@@ -1108,7 +1008,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Download all coverage artifacts
uses: actions/download-artifact@v3
- name: Upload coverage to Codecov (full coverage)
@@ -1119,7 +1019,6 @@ jobs:
with: |
fail_ci_if_error: true
flags: full-suite
token: ${{ env.CODECOV_TOKEN }}
attempt_limit: 5
attempt_delay: 30000
- name: Upload coverage to Codecov (partial coverage)
@@ -1129,6 +1028,5 @@ jobs:
action: codecov/codecov-action@v3.1.3
with: |
fail_ci_if_error: true
token: ${{ env.CODECOV_TOKEN }}
attempt_limit: 5
attempt_delay: 30000

View File

@@ -42,7 +42,7 @@ jobs:
id: token
# Pinned to a specific version of the action for security reasons
# v1.7.0
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
with:
app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }}
private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }}

View File

@@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.7.0
uses: actions/setup-python@v4.6.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}

View File

@@ -26,7 +26,7 @@ jobs:
architectures: ${{ steps.info.outputs.architectures }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Get information
id: info
@@ -56,7 +56,7 @@ jobs:
echo "CI_BUILD=1"
echo "ENABLE_HEADLESS=1"
# Use C-Extension for SQLAlchemy
# Use C-Extension for sqlalchemy
echo "REQUIRE_SQLALCHEMY_CEXT=1"
) > .env_file
@@ -84,7 +84,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Download env_file
uses: actions/download-artifact@v3
@@ -97,7 +97,7 @@ jobs:
name: requirements_diff
- name: Build wheels
uses: home-assistant/wheels@2023.09.1
uses: home-assistant/wheels@2023.04.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -122,7 +122,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.0
uses: actions/checkout@v3.5.3
- name: Download env_file
uses: actions/download-artifact@v3
@@ -178,7 +178,7 @@ jobs:
sed -i "/numpy/d" homeassistant/package_constraints.txt
- name: Build wheels (part 1)
uses: home-assistant/wheels@2023.09.1
uses: home-assistant/wheels@2023.04.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -186,13 +186,13 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
skip-binary: aiohttp;grpcio;sqlalchemy;protobuf
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa"
- name: Build wheels (part 2)
uses: home-assistant/wheels@2023.09.1
uses: home-assistant/wheels@2023.04.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -200,13 +200,13 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
skip-binary: aiohttp;grpcio;sqlalchemy;protobuf
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab"
- name: Build wheels (part 3)
uses: home-assistant/wheels@2023.09.1
uses: home-assistant/wheels@2023.04.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -214,7 +214,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
skip-binary: aiohttp;grpcio;sqlalchemy;protobuf
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac"

1
.gitignore vendored
View File

@@ -67,7 +67,6 @@ htmlcov/
test-reports/
test-results.xml
test-output.xml
pytest-*.txt
# Translations
*.mo

View File

@@ -1,12 +1,12 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.289
rev: v0.0.272
hooks:
- id: ruff
args:
- --fix
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.9.1
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
args:
@@ -17,11 +17,11 @@ repos:
hooks:
- id: codespell
args:
- --ignore-words-list=additionals,alle,alot,bund,currenty,datas,farenheit,falsy,fo,haa,hass,iif,incomfort,ines,ist,nam,nd,pres,pullrequests,resset,rime,ser,serie,te,technik,ue,unsecure,withing,zar
- --skip="./.*,*.csv,*.json,*.ambr"
- --ignore-words-list=additionals,alle,alot,ba,bre,bund,currenty,datas,dof,dur,ether,farenheit,falsy,fo,haa,hass,hist,iam,iff,iif,incomfort,ines,ist,lightsensor,mut,nam,nd,pres,pullrequests,referer,resset,rime,ser,serie,sur,te,technik,ue,uint,unsecure,visability,wan,wanna,withing,zar
- --skip="./.*,*.csv,*.json"
- --quiet-level=2
exclude_types: [csv, json]
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
exclude: ^tests/fixtures/|homeassistant/generated/
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
@@ -35,7 +35,7 @@ repos:
- --branch=master
- --branch=rc
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.32.0
rev: v1.28.0
hooks:
- id: yamllint
- repo: https://github.com/pre-commit/mirrors-prettier
@@ -43,7 +43,7 @@ repos:
hooks:
- id: prettier
- repo: https://github.com/cdce8p/python-typing-update
rev: v0.6.0
rev: v0.5.0
hooks:
# Run `python-typing-update` hook manually from time to time
# to update python typing syntax.
@@ -52,7 +52,7 @@ repos:
- id: python-typing-update
stages: [manual]
args:
- --py311-plus
- --py310-plus
- --force
- --keep-updates
files: ^(homeassistant|tests|script)/.+\.py$

View File

@@ -53,7 +53,6 @@ homeassistant.components.airzone_cloud.*
homeassistant.components.aladdin_connect.*
homeassistant.components.alarm_control_panel.*
homeassistant.components.alert.*
homeassistant.components.alexa.*
homeassistant.components.amazon_polly.*
homeassistant.components.ambient_station.*
homeassistant.components.amcrest.*
@@ -88,7 +87,6 @@ homeassistant.components.camera.*
homeassistant.components.canary.*
homeassistant.components.clickatell.*
homeassistant.components.clicksend.*
homeassistant.components.climate.*
homeassistant.components.cloud.*
homeassistant.components.configurator.*
homeassistant.components.cover.*
@@ -105,19 +103,16 @@ homeassistant.components.dhcp.*
homeassistant.components.diagnostics.*
homeassistant.components.dlna_dmr.*
homeassistant.components.dnsip.*
homeassistant.components.doorbird.*
homeassistant.components.dormakaba_dkey.*
homeassistant.components.dsmr.*
homeassistant.components.dunehd.*
homeassistant.components.efergy.*
homeassistant.components.electrasmart.*
homeassistant.components.electric_kiwi.*
homeassistant.components.elgato.*
homeassistant.components.elkm1.*
homeassistant.components.emulated_hue.*
homeassistant.components.energy.*
homeassistant.components.esphome.*
homeassistant.components.event.*
homeassistant.components.evil_genius_labs.*
homeassistant.components.fan.*
homeassistant.components.fastdotcom.*
@@ -137,11 +132,9 @@ homeassistant.components.fully_kiosk.*
homeassistant.components.geo_location.*
homeassistant.components.geocaching.*
homeassistant.components.gios.*
homeassistant.components.glances.*
homeassistant.components.goalzero.*
homeassistant.components.google.*
homeassistant.components.google_sheets.*
homeassistant.components.gpsd.*
homeassistant.components.greeneye_monitor.*
homeassistant.components.group.*
homeassistant.components.guardian.*
@@ -152,7 +145,6 @@ homeassistant.components.history.*
homeassistant.components.homeassistant.exposed_entities
homeassistant.components.homeassistant.triggers.event
homeassistant.components.homeassistant_alerts.*
homeassistant.components.homeassistant_green.*
homeassistant.components.homeassistant_hardware.*
homeassistant.components.homeassistant_sky_connect.*
homeassistant.components.homeassistant_yellow.*
@@ -180,7 +172,6 @@ homeassistant.components.huawei_lte.*
homeassistant.components.hydrawise.*
homeassistant.components.hyperion.*
homeassistant.components.ibeacon.*
homeassistant.components.idasen_desk.*
homeassistant.components.image.*
homeassistant.components.image_processing.*
homeassistant.components.image_upload.*
@@ -188,9 +179,7 @@ homeassistant.components.imap.*
homeassistant.components.input_button.*
homeassistant.components.input_select.*
homeassistant.components.integration.*
homeassistant.components.ipp.*
homeassistant.components.iqvia.*
homeassistant.components.islamic_prayer_times.*
homeassistant.components.isy994.*
homeassistant.components.jellyfin.*
homeassistant.components.jewish_calendar.*
@@ -202,7 +191,6 @@ homeassistant.components.lacrosse.*
homeassistant.components.lacrosse_view.*
homeassistant.components.lametric.*
homeassistant.components.laundrify.*
homeassistant.components.lawn_mower.*
homeassistant.components.lcn.*
homeassistant.components.ld2410_ble.*
homeassistant.components.lidarr.*
@@ -214,14 +202,11 @@ homeassistant.components.local_ip.*
homeassistant.components.lock.*
homeassistant.components.logbook.*
homeassistant.components.logger.*
homeassistant.components.london_underground.*
homeassistant.components.lookin.*
homeassistant.components.luftdaten.*
homeassistant.components.mailbox.*
homeassistant.components.mastodon.*
homeassistant.components.matrix.*
homeassistant.components.matter.*
homeassistant.components.media_extractor.*
homeassistant.components.media_player.*
homeassistant.components.media_source.*
homeassistant.components.metoffice.*
@@ -260,10 +245,7 @@ homeassistant.components.peco.*
homeassistant.components.persistent_notification.*
homeassistant.components.pi_hole.*
homeassistant.components.ping.*
homeassistant.components.plugwise.*
homeassistant.components.poolsense.*
homeassistant.components.powerwall.*
homeassistant.components.private_ble_device.*
homeassistant.components.proximity.*
homeassistant.components.prusalink.*
homeassistant.components.pure_energie.*
@@ -312,7 +294,6 @@ homeassistant.components.sonarr.*
homeassistant.components.speedtestdotnet.*
homeassistant.components.sql.*
homeassistant.components.ssdp.*
homeassistant.components.starlink.*
homeassistant.components.statistics.*
homeassistant.components.steamist.*
homeassistant.components.stookalert.*
@@ -321,7 +302,6 @@ homeassistant.components.sun.*
homeassistant.components.surepetcare.*
homeassistant.components.switch.*
homeassistant.components.switchbee.*
homeassistant.components.switchbot_cloud.*
homeassistant.components.switcher_kis.*
homeassistant.components.synology_dsm.*
homeassistant.components.systemmonitor.*
@@ -339,11 +319,9 @@ homeassistant.components.tplink.*
homeassistant.components.tplink_omada.*
homeassistant.components.tractive.*
homeassistant.components.tradfri.*
homeassistant.components.trafikverket_camera.*
homeassistant.components.trafikverket_ferry.*
homeassistant.components.trafikverket_train.*
homeassistant.components.trafikverket_weatherstation.*
homeassistant.components.trend.*
homeassistant.components.tts.*
homeassistant.components.twentemilieu.*
homeassistant.components.unifi.*

View File

@@ -19,6 +19,8 @@ build.json @home-assistant/supervisor
# Other code
/homeassistant/scripts/check_config.py @kellerza
/homeassistant/const.py @epenet
/homeassistant/util/ @epenet
# Integrations
/homeassistant/components/abode/ @shred86
@@ -47,10 +49,10 @@ build.json @home-assistant/supervisor
/tests/components/airq/ @Sibgatulin @dl2080
/homeassistant/components/airthings/ @danielhiversen
/tests/components/airthings/ @danielhiversen
/homeassistant/components/airthings_ble/ @vincegio @LaStrada
/tests/components/airthings_ble/ @vincegio @LaStrada
/homeassistant/components/airtouch4/ @samsinnamon
/tests/components/airtouch4/ @samsinnamon
/homeassistant/components/airthings_ble/ @vincegio
/tests/components/airthings_ble/ @vincegio
/homeassistant/components/airtouch4/ @LonePurpleWolf
/tests/components/airtouch4/ @LonePurpleWolf
/homeassistant/components/airvisual/ @bachya
/tests/components/airvisual/ @bachya
/homeassistant/components/airvisual_pro/ @bachya
@@ -193,8 +195,8 @@ build.json @home-assistant/supervisor
/tests/components/camera/ @home-assistant/core
/homeassistant/components/cast/ @emontnemery
/tests/components/cast/ @emontnemery
/homeassistant/components/cert_expiry/ @jjlawren
/tests/components/cert_expiry/ @jjlawren
/homeassistant/components/cert_expiry/ @Cereal2nd @jjlawren
/tests/components/cert_expiry/ @Cereal2nd @jjlawren
/homeassistant/components/circuit/ @braam
/homeassistant/components/cisco_ios/ @fbradyirl
/homeassistant/components/cisco_mobility_express/ @fbradyirl
@@ -205,14 +207,10 @@ build.json @home-assistant/supervisor
/tests/components/cloud/ @home-assistant/cloud
/homeassistant/components/cloudflare/ @ludeeus @ctalkington
/tests/components/cloudflare/ @ludeeus @ctalkington
/homeassistant/components/co2signal/ @jpbede
/tests/components/co2signal/ @jpbede
/homeassistant/components/coinbase/ @tombrien
/tests/components/coinbase/ @tombrien
/homeassistant/components/color_extractor/ @GenericStudent
/tests/components/color_extractor/ @GenericStudent
/homeassistant/components/comelit/ @chemelli74
/tests/components/comelit/ @chemelli74
/homeassistant/components/comfoconnect/ @michaelarnauts
/tests/components/comfoconnect/ @michaelarnauts
/homeassistant/components/command_line/ @gjohansson-ST
@@ -279,6 +277,8 @@ build.json @home-assistant/supervisor
/tests/components/discord/ @tkdrob
/homeassistant/components/discovergy/ @jpbede
/tests/components/discovergy/ @jpbede
/homeassistant/components/discovery/ @home-assistant/core
/tests/components/discovery/ @home-assistant/core
/homeassistant/components/dlink/ @tkdrob
/tests/components/dlink/ @tkdrob
/homeassistant/components/dlna_dmr/ @StevenLooman @chishm
@@ -297,10 +297,10 @@ build.json @home-assistant/supervisor
/tests/components/dsmr/ @Robbie1221 @frenck
/homeassistant/components/dsmr_reader/ @depl0y @glodenox
/tests/components/dsmr_reader/ @depl0y @glodenox
/homeassistant/components/duotecno/ @cereal2nd
/tests/components/duotecno/ @cereal2nd
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
/tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @andarotajo
/homeassistant/components/dunehd/ @bieniu
/tests/components/dunehd/ @bieniu
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo
/tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo
/homeassistant/components/dynalite/ @ziv1234
/tests/components/dynalite/ @ziv1234
/homeassistant/components/eafm/ @Jc2k
@@ -309,8 +309,6 @@ build.json @home-assistant/supervisor
/tests/components/easyenergy/ @klaasnicolaas
/homeassistant/components/ecobee/ @marthoc @marcolivierarsenault
/tests/components/ecobee/ @marthoc @marcolivierarsenault
/homeassistant/components/ecoforest/ @pjanuario
/tests/components/ecoforest/ @pjanuario
/homeassistant/components/econet/ @vangorra @w1ll1am23
/tests/components/econet/ @vangorra @w1ll1am23
/homeassistant/components/ecovacs/ @OverloadUT @mib1185
@@ -323,8 +321,6 @@ build.json @home-assistant/supervisor
/tests/components/eight_sleep/ @mezz64 @raman325
/homeassistant/components/electrasmart/ @jafar-atili
/tests/components/electrasmart/ @jafar-atili
/homeassistant/components/electric_kiwi/ @mikey0000
/tests/components/electric_kiwi/ @mikey0000
/homeassistant/components/elgato/ @frenck
/tests/components/elgato/ @frenck
/homeassistant/components/elkm1/ @gwww @bdraco
@@ -347,8 +343,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/enigma2/ @fbradyirl
/homeassistant/components/enocean/ @bdurrer
/tests/components/enocean/ @bdurrer
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek
/tests/components/enphase_envoy/ @bdraco @cgarwood @dgomes @joostlek
/homeassistant/components/enphase_envoy/ @gtdiehl
/tests/components/enphase_envoy/ @gtdiehl
/homeassistant/components/entur_public_transport/ @hfurubotten
/homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie
@@ -360,12 +356,10 @@ build.json @home-assistant/supervisor
/homeassistant/components/eq3btsmart/ @rytilahti
/homeassistant/components/escea/ @lazdavila
/tests/components/escea/ @lazdavila
/homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
/tests/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
/homeassistant/components/esphome/ @OttoWinter @jesserockz @bdraco
/tests/components/esphome/ @OttoWinter @jesserockz @bdraco
/homeassistant/components/eufylife_ble/ @bdr99
/tests/components/eufylife_ble/ @bdr99
/homeassistant/components/event/ @home-assistant/core
/tests/components/event/ @home-assistant/core
/homeassistant/components/evil_genius_labs/ @balloob
/tests/components/evil_genius_labs/ @balloob
/homeassistant/components/evohome/ @zxdavb
@@ -390,8 +384,6 @@ build.json @home-assistant/supervisor
/tests/components/fireservicerota/ @cyberjunky
/homeassistant/components/firmata/ @DaAwesomeP
/tests/components/firmata/ @DaAwesomeP
/homeassistant/components/fitbit/ @allenporter
/tests/components/fitbit/ @allenporter
/homeassistant/components/fivem/ @Sander0542
/tests/components/fivem/ @Sander0542
/homeassistant/components/fjaraskupan/ @elupus
@@ -404,8 +396,8 @@ build.json @home-assistant/supervisor
/tests/components/flo/ @dmulcahey
/homeassistant/components/flume/ @ChrisMandich @bdraco @jeeftor
/tests/components/flume/ @ChrisMandich @bdraco @jeeftor
/homeassistant/components/flux_led/ @icemanch
/tests/components/flux_led/ @icemanch
/homeassistant/components/flux_led/ @icemanch @bdraco
/tests/components/flux_led/ @icemanch @bdraco
/homeassistant/components/forecast_solar/ @klaasnicolaas @frenck
/tests/components/forecast_solar/ @klaasnicolaas @frenck
/homeassistant/components/forked_daapd/ @uvjustin
@@ -433,8 +425,6 @@ build.json @home-assistant/supervisor
/tests/components/fully_kiosk/ @cgarwood
/homeassistant/components/garages_amsterdam/ @klaasnicolaas
/tests/components/garages_amsterdam/ @klaasnicolaas
/homeassistant/components/gardena_bluetooth/ @elupus
/tests/components/gardena_bluetooth/ @elupus
/homeassistant/components/gdacs/ @exxamalte
/tests/components/gdacs/ @exxamalte
/homeassistant/components/generic/ @davet2001
@@ -529,8 +519,6 @@ build.json @home-assistant/supervisor
/tests/components/homeassistant/ @home-assistant/core
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
/tests/components/homeassistant_alerts/ @home-assistant/core
/homeassistant/components/homeassistant_green/ @home-assistant/core
/tests/components/homeassistant_green/ @home-assistant/core
/homeassistant/components/homeassistant_hardware/ @home-assistant/core
/tests/components/homeassistant_hardware/ @home-assistant/core
/homeassistant/components/homeassistant_sky_connect/ @home-assistant/core
@@ -562,7 +550,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/hvv_departures/ @vigonotion
/tests/components/hvv_departures/ @vigonotion
/homeassistant/components/hydrawise/ @dknowles2 @ptcryan
/tests/components/hydrawise/ @dknowles2 @ptcryan
/homeassistant/components/hyperion/ @dermotduffy
/tests/components/hyperion/ @dermotduffy
/homeassistant/components/ialarm/ @RyuzakiKK
@@ -574,8 +561,6 @@ build.json @home-assistant/supervisor
/tests/components/ibeacon/ @bdraco
/homeassistant/components/icloud/ @Quentame @nzapponi
/tests/components/icloud/ @Quentame @nzapponi
/homeassistant/components/idasen_desk/ @abmantis
/tests/components/idasen_desk/ @abmantis
/homeassistant/components/ign_sismologia/ @exxamalte
/tests/components/ign_sismologia/ @exxamalte
/homeassistant/components/image/ @home-assistant/core
@@ -617,8 +602,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
/tests/components/iotawatt/ @gtdiehl @jyavenard
/homeassistant/components/iperf3/ @rohankapoorcom
/homeassistant/components/ipma/ @dgomes
/tests/components/ipma/ @dgomes
/homeassistant/components/ipma/ @dgomes @abmantis
/tests/components/ipma/ @dgomes @abmantis
/homeassistant/components/ipp/ @ctalkington
/tests/components/ipp/ @ctalkington
/homeassistant/components/iqvia/ @bachya
@@ -682,8 +667,6 @@ build.json @home-assistant/supervisor
/tests/components/launch_library/ @ludeeus @DurgNomis-drol
/homeassistant/components/laundrify/ @xLarry
/tests/components/laundrify/ @xLarry
/homeassistant/components/lawn_mower/ @home-assistant/core
/tests/components/lawn_mower/ @home-assistant/core
/homeassistant/components/lcn/ @alengwenus
/tests/components/lcn/ @alengwenus
/homeassistant/components/ld2410_ble/ @930913
@@ -695,6 +678,8 @@ build.json @home-assistant/supervisor
/tests/components/lidarr/ @tkdrob
/homeassistant/components/life360/ @pnbruckner
/tests/components/life360/ @pnbruckner
/homeassistant/components/lifx/ @bdraco
/tests/components/lifx/ @bdraco
/homeassistant/components/light/ @home-assistant/core
/tests/components/light/ @home-assistant/core
/homeassistant/components/linux_battery/ @fabaff
@@ -716,8 +701,6 @@ build.json @home-assistant/supervisor
/tests/components/logger/ @home-assistant/core
/homeassistant/components/logi_circle/ @evanjd
/tests/components/logi_circle/ @evanjd
/homeassistant/components/london_underground/ @jpbede
/tests/components/london_underground/ @jpbede
/homeassistant/components/lookin/ @ANMalko @bdraco
/tests/components/lookin/ @ANMalko @bdraco
/homeassistant/components/loqed/ @mikewoudenberg
@@ -734,16 +717,12 @@ build.json @home-assistant/supervisor
/homeassistant/components/lyric/ @timmo001
/tests/components/lyric/ @timmo001
/homeassistant/components/mastodon/ @fabaff
/homeassistant/components/matrix/ @PaarthShah
/tests/components/matrix/ @PaarthShah
/homeassistant/components/matter/ @home-assistant/matter
/tests/components/matter/ @home-assistant/matter
/homeassistant/components/mazda/ @bdr99
/tests/components/mazda/ @bdr99
/homeassistant/components/meater/ @Sotolotl @emontnemery
/tests/components/meater/ @Sotolotl @emontnemery
/homeassistant/components/medcom_ble/ @elafargue
/tests/components/medcom_ble/ @elafargue
/homeassistant/components/media_extractor/ @joostlek
/tests/components/media_extractor/ @joostlek
/homeassistant/components/media_player/ @home-assistant/core
/tests/components/media_player/ @home-assistant/core
/homeassistant/components/media_source/ @hunterjm
@@ -766,6 +745,7 @@ build.json @home-assistant/supervisor
/tests/components/meteoclimatic/ @adrianmo
/homeassistant/components/metoffice/ @MrHarcombe @avee87
/tests/components/metoffice/ @MrHarcombe @avee87
/homeassistant/components/miflora/ @danielhiversen @basnijholt
/homeassistant/components/mikrotik/ @engrbm87
/tests/components/mikrotik/ @engrbm87
/homeassistant/components/mill/ @danielhiversen
@@ -780,8 +760,8 @@ build.json @home-assistant/supervisor
/tests/components/moat/ @bdraco
/homeassistant/components/mobile_app/ @home-assistant/core
/tests/components/mobile_app/ @home-assistant/core
/homeassistant/components/modbus/ @janiversen
/tests/components/modbus/ @janiversen
/homeassistant/components/modbus/ @adamchengtkc @janiversen @vzahradnik
/tests/components/modbus/ @adamchengtkc @janiversen @vzahradnik
/homeassistant/components/modem_callerid/ @tkdrob
/tests/components/modem_callerid/ @tkdrob
/homeassistant/components/modern_forms/ @wonderslug
@@ -807,8 +787,8 @@ build.json @home-assistant/supervisor
/tests/components/mutesync/ @currentoor
/homeassistant/components/my/ @home-assistant/core
/tests/components/my/ @home-assistant/core
/homeassistant/components/myq/ @ehendrix23 @Lash-L
/tests/components/myq/ @ehendrix23 @Lash-L
/homeassistant/components/myq/ @ehendrix23
/tests/components/myq/ @ehendrix23
/homeassistant/components/mysensors/ @MartinHjelmare @functionpointer
/tests/components/mysensors/ @MartinHjelmare @functionpointer
/homeassistant/components/mystrom/ @fabaff
@@ -909,7 +889,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/openhome/ @bazwilliams
/tests/components/openhome/ @bazwilliams
/homeassistant/components/opensky/ @joostlek
/tests/components/opensky/ @joostlek
/homeassistant/components/opentherm_gw/ @mvn23
/tests/components/opentherm_gw/ @mvn23
/homeassistant/components/openuv/ @bachya
@@ -937,8 +916,6 @@ build.json @home-assistant/supervisor
/tests/components/panel_iframe/ @home-assistant/frontend
/homeassistant/components/peco/ @IceBotYT
/tests/components/peco/ @IceBotYT
/homeassistant/components/pegel_online/ @mib1185
/tests/components/pegel_online/ @mib1185
/homeassistant/components/persistent_notification/ @home-assistant/core
/tests/components/persistent_notification/ @home-assistant/core
/homeassistant/components/philips_js/ @elupus
@@ -963,8 +940,6 @@ build.json @home-assistant/supervisor
/tests/components/poolsense/ @haemishkyd
/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson
/tests/components/powerwall/ @bdraco @jrester @daniel-simpson
/homeassistant/components/private_ble_device/ @Jc2k
/tests/components/private_ble_device/ @Jc2k
/homeassistant/components/profiler/ @bdraco
/tests/components/profiler/ @bdraco
/homeassistant/components/progettihwsw/ @ardaseremet
@@ -1045,6 +1020,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/repairs/ @home-assistant/core
/tests/components/repairs/ @home-assistant/core
/homeassistant/components/repetier/ @MTrab @ShadowBr0ther
/homeassistant/components/rest/ @epenet
/tests/components/rest/ @epenet
/homeassistant/components/rflink/ @javicalle
/tests/components/rflink/ @javicalle
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
@@ -1073,8 +1050,8 @@ build.json @home-assistant/supervisor
/tests/components/rss_feed_template/ @home-assistant/core
/homeassistant/components/rtsp_to_webrtc/ @allenporter
/tests/components/rtsp_to_webrtc/ @allenporter
/homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
/homeassistant/components/ruckus_unleashed/ @gabe565
/tests/components/ruckus_unleashed/ @gabe565
/homeassistant/components/ruuvi_gateway/ @akx
/tests/components/ruuvi_gateway/ @akx
/homeassistant/components/ruuvitag_ble/ @akx
@@ -1092,11 +1069,9 @@ build.json @home-assistant/supervisor
/tests/components/scene/ @home-assistant/core
/homeassistant/components/schedule/ @home-assistant/core
/tests/components/schedule/ @home-assistant/core
/homeassistant/components/schlage/ @dknowles2
/tests/components/schlage/ @dknowles2
/homeassistant/components/schluter/ @prairieapps
/homeassistant/components/scrape/ @fabaff @gjohansson-ST
/tests/components/scrape/ @fabaff @gjohansson-ST
/homeassistant/components/scrape/ @fabaff @gjohansson-ST @epenet
/tests/components/scrape/ @fabaff @gjohansson-ST @epenet
/homeassistant/components/screenlogic/ @dieselrabbit @bdraco
/tests/components/screenlogic/ @dieselrabbit @bdraco
/homeassistant/components/script/ @home-assistant/core
@@ -1151,8 +1126,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/sky_hub/ @rogerselwyn
/homeassistant/components/skybell/ @tkdrob
/tests/components/skybell/ @tkdrob
/homeassistant/components/slack/ @tkdrob @fletcherau
/tests/components/slack/ @tkdrob @fletcherau
/homeassistant/components/slack/ @tkdrob
/tests/components/slack/ @tkdrob
/homeassistant/components/sleepiq/ @mfugate1 @kbickar
/tests/components/sleepiq/ @mfugate1 @kbickar
/homeassistant/components/slide/ @ualex73
@@ -1200,8 +1175,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/spider/ @peternijssen
/tests/components/spider/ @peternijssen
/homeassistant/components/splunk/ @Bre77
/homeassistant/components/spotify/ @frenck @joostlek
/tests/components/spotify/ @frenck @joostlek
/homeassistant/components/spotify/ @frenck
/tests/components/spotify/ @frenck
/homeassistant/components/sql/ @gjohansson-ST @dougiteixeira
/tests/components/sql/ @gjohansson-ST @dougiteixeira
/homeassistant/components/squeezebox/ @rajlaud
@@ -1243,10 +1218,8 @@ build.json @home-assistant/supervisor
/tests/components/switch_as_x/ @home-assistant/core
/homeassistant/components/switchbee/ @jafar-atili
/tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/homeassistant/components/switchbot_cloud/ @SeraphicRav
/tests/components/switchbot_cloud/ @SeraphicRav
/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/homeassistant/components/switcher_kis/ @thecode
/tests/components/switcher_kis/ @thecode
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
@@ -1317,8 +1290,6 @@ build.json @home-assistant/supervisor
/tests/components/trace/ @home-assistant/core
/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu
/tests/components/tractive/ @Danielhiversen @zhulik @bieniu
/homeassistant/components/trafikverket_camera/ @gjohansson-ST
/tests/components/trafikverket_camera/ @gjohansson-ST
/homeassistant/components/trafikverket_ferry/ @gjohansson-ST
/tests/components/trafikverket_ferry/ @gjohansson-ST
/homeassistant/components/trafikverket_train/ @endor-force @gjohansson-ST
@@ -1327,16 +1298,14 @@ build.json @home-assistant/supervisor
/tests/components/trafikverket_weatherstation/ @endor-force @gjohansson-ST
/homeassistant/components/transmission/ @engrbm87 @JPHutchins
/tests/components/transmission/ @engrbm87 @JPHutchins
/homeassistant/components/trend/ @jpbede
/tests/components/trend/ @jpbede
/homeassistant/components/tts/ @home-assistant/core @pvizeli
/tests/components/tts/ @home-assistant/core @pvizeli
/homeassistant/components/tuya/ @Tuya @zlinoliver @frenck
/tests/components/tuya/ @Tuya @zlinoliver @frenck
/homeassistant/components/twentemilieu/ @frenck
/tests/components/twentemilieu/ @frenck
/homeassistant/components/twinkly/ @dr1rrb @Robbie1221 @Olen
/tests/components/twinkly/ @dr1rrb @Robbie1221 @Olen
/homeassistant/components/twinkly/ @dr1rrb @Robbie1221
/tests/components/twinkly/ @dr1rrb @Robbie1221
/homeassistant/components/twitch/ @joostlek
/tests/components/twitch/ @joostlek
/homeassistant/components/ukraine_alarm/ @PaulAnnekov
@@ -1372,11 +1341,11 @@ build.json @home-assistant/supervisor
/homeassistant/components/velbus/ @Cereal2nd @brefra
/tests/components/velbus/ @Cereal2nd @brefra
/homeassistant/components/velux/ @Julius2342
/homeassistant/components/venstar/ @garbled1 @jhollowe
/tests/components/venstar/ @garbled1 @jhollowe
/homeassistant/components/verisure/ @frenck
/tests/components/verisure/ @frenck
/homeassistant/components/versasense/ @imstevenxyz
/homeassistant/components/venstar/ @garbled1
/tests/components/venstar/ @garbled1
/homeassistant/components/verisure/ @frenck @niro1987
/tests/components/verisure/ @frenck @niro1987
/homeassistant/components/versasense/ @flamm3blemuff1n
/homeassistant/components/version/ @ludeeus
/tests/components/version/ @ludeeus
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey
@@ -1388,8 +1357,6 @@ build.json @home-assistant/supervisor
/tests/components/vizio/ @raman325
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
/tests/components/vlc_telnet/ @rodripf @MartinHjelmare
/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74
/tests/components/vodafone_station/ @paoloantinori @chemelli74
/homeassistant/components/voip/ @balloob @synesthesiam
/tests/components/voip/ @balloob @synesthesiam
/homeassistant/components/volumio/ @OnFreund
@@ -1400,12 +1367,9 @@ build.json @home-assistant/supervisor
/tests/components/vulcan/ @Antoni-Czaplicki
/homeassistant/components/wake_on_lan/ @ntilley905
/tests/components/wake_on_lan/ @ntilley905
/homeassistant/components/wake_word/ @home-assistant/core @synesthesiam
/tests/components/wake_word/ @home-assistant/core @synesthesiam
/homeassistant/components/wallbox/ @hesselonline
/tests/components/wallbox/ @hesselonline
/homeassistant/components/waqi/ @joostlek
/tests/components/waqi/ @joostlek
/homeassistant/components/waqi/ @andrey-git
/homeassistant/components/water_heater/ @home-assistant/core
/tests/components/water_heater/ @home-assistant/core
/homeassistant/components/watson_tts/ @rutkai
@@ -1415,10 +1379,6 @@ build.json @home-assistant/supervisor
/tests/components/waze_travel_time/ @eifinger
/homeassistant/components/weather/ @home-assistant/core
/tests/components/weather/ @home-assistant/core
/homeassistant/components/weatherflow/ @natekspencer @jeeftor
/tests/components/weatherflow/ @natekspencer @jeeftor
/homeassistant/components/weatherkit/ @tjhorner
/tests/components/weatherkit/ @tjhorner
/homeassistant/components/webhook/ @home-assistant/core
/tests/components/webhook/ @home-assistant/core
/homeassistant/components/webostv/ @thecode
@@ -1436,8 +1396,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/wilight/ @leofig-rj
/tests/components/wilight/ @leofig-rj
/homeassistant/components/wirelesstag/ @sergeymaysak
/homeassistant/components/withings/ @vangorra @joostlek
/tests/components/withings/ @vangorra @joostlek
/homeassistant/components/withings/ @vangorra
/tests/components/withings/ @vangorra
/homeassistant/components/wiz/ @sbidy
/tests/components/wiz/ @sbidy
/homeassistant/components/wled/ @frenck
@@ -1470,8 +1430,6 @@ build.json @home-assistant/supervisor
/tests/components/yamaha_musiccast/ @vigonotion @micha91
/homeassistant/components/yandex_transport/ @rishatik92 @devbis
/tests/components/yandex_transport/ @rishatik92 @devbis
/homeassistant/components/yardian/ @h3l1o5
/tests/components/yardian/ @h3l1o5
/homeassistant/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
/tests/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
/homeassistant/components/yeelightsunflower/ @lindsaymarkward

View File

@@ -15,8 +15,9 @@ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
RUN \
pip3 install \
--no-cache-dir \
--no-index \
--only-binary=:all: \
--index-url "https://wheels.home-assistant.io/musllinux-index/" \
--find-links "${WHEELS_LINKS}" \
-r homeassistant/requirements.txt
COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/
@@ -38,8 +39,9 @@ RUN \
MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \
pip3 install \
--no-cache-dir \
--no-index \
--only-binary=:all: \
--index-url "https://wheels.home-assistant.io/musllinux-index/" \
--find-links "${WHEELS_LINKS}" \
-r homeassistant/requirements_all.txt
## Setup Home Assistant Core
@@ -47,8 +49,9 @@ COPY . homeassistant/
RUN \
pip3 install \
--no-cache-dir \
--no-index \
--only-binary=:all: \
--index-url "https://wheels.home-assistant.io/musllinux-index/" \
--find-links "${WHEELS_LINKS}" \
-e ./homeassistant \
&& python3 -m compileall \
homeassistant/homeassistant

View File

@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.09.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.09.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.09.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.09.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.09.0
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.06.1
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.06.1
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.06.1
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.06.1
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.06.1
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io

View File

@@ -148,6 +148,16 @@ def get_arguments() -> argparse.Namespace:
return arguments
def cmdline() -> list[str]:
"""Collect path and arguments to re-execute the current hass instance."""
if os.path.basename(sys.argv[0]) == "__main__.py":
modulepath = os.path.dirname(sys.argv[0])
os.environ["PYTHONPATH"] = os.path.dirname(modulepath)
return [sys.executable, "-m", "homeassistant"] + list(sys.argv[1:])
return sys.argv
def check_threads() -> None:
"""Check if there are any lingering threads."""
try:

View File

@@ -1,15 +1,32 @@
"""Enum backports from standard lib.
This file contained the backport of the StrEnum of Python 3.11.
Since we have dropped support for Python 3.10, we can remove this backport.
This file is kept for now to avoid breaking custom components that might
import it.
"""
"""Enum backports from standard lib."""
from __future__ import annotations
from enum import StrEnum
from enum import Enum
from typing import Any
__all__ = [
"StrEnum",
]
from typing_extensions import Self
class StrEnum(str, Enum):
"""Partial backport of Python 3.11's StrEnum for our basic use cases."""
def __new__(cls, value: str, *args: Any, **kwargs: Any) -> Self:
"""Create a new StrEnum instance."""
if not isinstance(value, str):
raise TypeError(f"{value!r} is not a string")
return super().__new__(cls, value, *args, **kwargs)
def __str__(self) -> str:
"""Return self.value."""
return str(self.value)
@staticmethod
def _generate_next_value_(
name: str, start: int, count: int, last_values: list[Any]
) -> Any:
"""Make `auto()` explicitly unsupported.
We may revisit this when it's very clear that Python 3.11's
`StrEnum.auto()` behavior will no longer change.
"""
raise TypeError("auto() is not supported by this implementation")

View File

@@ -3,24 +3,27 @@ from __future__ import annotations
from collections.abc import Callable
from types import GenericAlias
from typing import Any, Generic, Self, TypeVar, overload
from typing import Any, Generic, TypeVar, overload
from typing_extensions import Self
_T = TypeVar("_T")
_R = TypeVar("_R")
class cached_property(Generic[_T]):
class cached_property(Generic[_T, _R]): # pylint: disable=invalid-name
"""Backport of Python 3.12's cached_property.
Includes https://github.com/python/cpython/pull/101890/files
"""
def __init__(self, func: Callable[[Any], _T]) -> None:
def __init__(self, func: Callable[[_T], _R]) -> None:
"""Initialize."""
self.func: Callable[[Any], _T] = func
self.attrname: str | None = None
self.func = func
self.attrname: Any = None
self.__doc__ = func.__doc__
def __set_name__(self, owner: type[Any], name: str) -> None:
def __set_name__(self, owner: type[_T], name: str) -> None:
"""Set name."""
if self.attrname is None:
self.attrname = name
@@ -31,16 +34,14 @@ class cached_property(Generic[_T]):
)
@overload
def __get__(self, instance: None, owner: type[Any] | None = None) -> Self:
def __get__(self, instance: None, owner: type[_T]) -> Self:
...
@overload
def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T:
def __get__(self, instance: _T, owner: type[_T]) -> _R:
...
def __get__(
self, instance: Any | None, owner: type[Any] | None = None
) -> _T | Self:
def __get__(self, instance: _T | None, owner: type[_T] | None = None) -> _R | Self:
"""Get."""
if instance is None:
return self

View File

@@ -110,7 +110,8 @@ async def async_setup_hass(
runtime_config: RuntimeConfig,
) -> core.HomeAssistant | None:
"""Set up Home Assistant."""
hass = core.HomeAssistant(runtime_config.config_dir)
hass = core.HomeAssistant()
hass.config.config_dir = runtime_config.config_dir
async_enable_logging(
hass,
@@ -133,7 +134,6 @@ async def async_setup_hass(
_LOGGER.info("Config directory: %s", runtime_config.config_dir)
loader.async_setup(hass)
config_dict = None
basic_setup_success = False
@@ -177,15 +177,14 @@ async def async_setup_hass(
old_config = hass.config
old_logging = hass.data.get(DATA_LOGGING)
hass = core.HomeAssistant(old_config.config_dir)
hass = core.HomeAssistant()
if old_logging:
hass.data[DATA_LOGGING] = old_logging
hass.config.skip_pip = old_config.skip_pip
hass.config.skip_pip_packages = old_config.skip_pip_packages
hass.config.internal_url = old_config.internal_url
hass.config.external_url = old_config.external_url
# Setup loader cache after the config dir has been set
loader.async_setup(hass)
hass.config.config_dir = old_config.config_dir
if safe_mode:
_LOGGER.info("Starting in safe mode")

View File

@@ -7,7 +7,6 @@
"homekit",
"ibeacon",
"icloud",
"itunes",
"weatherkit"
"itunes"
]
}

View File

@@ -1,5 +1,5 @@
{
"domain": "ikea",
"name": "IKEA",
"integrations": ["symfonisk", "tradfri", "idasen_desk"]
"integrations": ["symfonisk", "tradfri"]
}

View File

@@ -1,5 +0,0 @@
{
"domain": "switchbot",
"name": "SwitchBot",
"integrations": ["switchbot", "switchbot_cloud"]
}

View File

@@ -2,7 +2,6 @@
"domain": "trafikverket",
"name": "Trafikverket",
"integrations": [
"trafikverket_camera",
"trafikverket_ferry",
"trafikverket_train",
"trafikverket_weatherstation"

View File

@@ -28,7 +28,6 @@ from homeassistant.const import (
from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, entity
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import dispatcher_send
from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER
@@ -288,14 +287,14 @@ class AbodeDevice(AbodeEntity):
"""Initialize Abode device."""
super().__init__(data)
self._device = device
self._attr_unique_id = device.uuid
self._attr_unique_id = device.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._device.device_id,
self._update_callback,
)
@@ -303,7 +302,7 @@ class AbodeDevice(AbodeEntity):
"""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
self._data.abode.events.remove_all_device_callbacks, self._device.device_id
)
def update(self) -> None:
@@ -314,17 +313,17 @@ class AbodeDevice(AbodeEntity):
def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes."""
return {
"device_id": self._device.id,
"device_id": self._device.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:
def device_info(self) -> entity.DeviceInfo:
"""Return device registry information for this entity."""
return DeviceInfo(
identifiers={(DOMAIN, self._device.id)},
return entity.DeviceInfo(
identifiers={(DOMAIN, self._device.device_id)},
manufacturer="Abode",
model=self._device.type,
name=self._device.name,

View File

@@ -69,7 +69,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes."""
return {
"device_id": self._device.id,
"device_id": self._device.device_id,
"battery_backup": self._device.battery,
"cellular_backup": self._device.is_cellular,
}

View File

@@ -30,7 +30,7 @@ async def async_setup_entry(
data: AbodeSystem = hass.data[DOMAIN]
async_add_entities(
AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)
AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) # pylint: disable=no-member
for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA)
)

View File

@@ -1,8 +1,6 @@
"""Support for Abode Security System sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import cast
from jaraco.abode.devices.sensor import Sensor as AbodeSense
@@ -14,52 +12,25 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import LIGHT_LUX, PERCENTAGE, UnitOfTemperature
from homeassistant.const import LIGHT_LUX
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AbodeDevice, AbodeSystem
from .const import DOMAIN
ABODE_TEMPERATURE_UNIT_HA_UNIT = {
CONST.UNIT_FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,
CONST.UNIT_CELSIUS: UnitOfTemperature.CELSIUS,
}
@dataclass
class AbodeSensorDescriptionMixin:
"""Mixin for Abode sensor."""
value_fn: Callable[[AbodeSense], float]
native_unit_of_measurement_fn: Callable[[AbodeSense], str]
@dataclass
class AbodeSensorDescription(SensorEntityDescription, AbodeSensorDescriptionMixin):
"""Class describing Abode sensor entities."""
SENSOR_TYPES: tuple[AbodeSensorDescription, ...] = (
AbodeSensorDescription(
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=CONST.TEMP_STATUS_KEY,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement_fn=lambda device: ABODE_TEMPERATURE_UNIT_HA_UNIT[
device.temp_unit
],
value_fn=lambda device: cast(float, device.temp),
),
AbodeSensorDescription(
SensorEntityDescription(
key=CONST.HUMI_STATUS_KEY,
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
value_fn=lambda device: cast(float, device.humidity),
),
AbodeSensorDescription(
SensorEntityDescription(
key=CONST.LUX_STATUS_KEY,
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement_fn=lambda _: LIGHT_LUX,
value_fn=lambda device: cast(float, device.lux),
),
)
@@ -81,26 +52,32 @@ async def async_setup_entry(
class AbodeSensor(AbodeDevice, SensorEntity):
"""A sensor implementation for Abode devices."""
entity_description: AbodeSensorDescription
_device: AbodeSense
def __init__(
self,
data: AbodeSystem,
device: AbodeSense,
description: AbodeSensorDescription,
description: SensorEntityDescription,
) -> None:
"""Initialize a sensor for an Abode device."""
super().__init__(data, device)
self.entity_description = description
self._attr_unique_id = f"{device.uuid}-{description.key}"
self._attr_unique_id = f"{device.device_uuid}-{description.key}"
if description.key == CONST.TEMP_STATUS_KEY:
self._attr_native_unit_of_measurement = device.temp_unit
elif description.key == CONST.HUMI_STATUS_KEY:
self._attr_native_unit_of_measurement = device.humidity_unit
elif description.key == CONST.LUX_STATUS_KEY:
self._attr_native_unit_of_measurement = LIGHT_LUX
@property
def native_value(self) -> float:
def native_value(self) -> float | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self._device)
@property
def native_unit_of_measurement(self) -> str:
"""Return the native unit of measurement."""
return self.entity_description.native_unit_of_measurement_fn(self._device)
if self.entity_description.key == CONST.TEMP_STATUS_KEY:
return cast(float, self._device.temp)
if self.entity_description.key == CONST.HUMI_STATUS_KEY:
return cast(float, self._device.humidity)
if self.entity_description.key == CONST.LUX_STATUS_KEY:
return cast(float, self._device.lux)
return None

View File

@@ -1,6 +1,10 @@
capture_image:
name: Capture image
description: Request a new image capture from a camera device.
fields:
entity_id:
name: Entity
description: Entity id of the camera to request an image.
required: true
selector:
entity:
@@ -8,21 +12,31 @@ capture_image:
domain: camera
change_setting:
name: Change setting
description: Change an Abode system setting.
fields:
setting:
name: Setting
description: Setting to change.
required: true
example: beeper_mute
selector:
text:
value:
name: Value
description: Value of the setting.
required: true
example: "1"
selector:
text:
trigger_automation:
name: Trigger automation
description: Trigger an Abode automation.
fields:
entity_id:
name: Entity
description: Entity id of the automation to trigger.
required: true
selector:
entity:

View File

@@ -15,7 +15,7 @@
}
},
"reauth_confirm": {
"title": "[%key:component::abode::config::step::user::title%]",
"title": "Fill in your Abode login information",
"data": {
"username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
@@ -31,41 +31,5 @@
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"services": {
"capture_image": {
"name": "Capture image",
"description": "Request a new image capture from a camera device.",
"fields": {
"entity_id": {
"name": "Entity",
"description": "Entity id of the camera to request an image."
}
}
},
"change_setting": {
"name": "Change setting",
"description": "Change an Abode system setting.",
"fields": {
"setting": {
"name": "Setting",
"description": "Setting to change."
},
"value": {
"name": "Value",
"description": "Value of the setting."
}
}
},
"trigger_automation": {
"name": "Trigger automation",
"description": "Trigger an Abode automation.",
"fields": {
"entity_id": {
"name": "Entity",
"description": "Entity id of the automation to trigger."
}
}
}
}
}

View File

@@ -1,7 +1,6 @@
"""The AccuWeather component."""
from __future__ import annotations
from asyncio import timeout
from datetime import timedelta
import logging
from typing import Any
@@ -9,6 +8,7 @@ from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
from homeassistant.components.sensor import DOMAIN as SENSOR_PLATFORM
from homeassistant.config_entries import ConfigEntry
@@ -16,7 +16,8 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER

View File

@@ -2,12 +2,12 @@
from __future__ import annotations
import asyncio
from asyncio import timeout
from typing import Any
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientError
from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
import voluptuous as vol
from homeassistant import config_entries

View File

@@ -50,8 +50,3 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = {
ATTR_CONDITION_SUNNY: [1, 2, 5],
ATTR_CONDITION_WINDY: [32],
}
CONDITION_MAP = {
cond_code: cond_ha
for cond_ha, cond_codes in CONDITION_CLASSES.items()
for cond_code in cond_codes
}

View File

@@ -3,7 +3,7 @@
"name": "AccuWeather",
"codeowners": ["@bieniu"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/accuweather",
"documentation": "https://www.home-assistant.io/integrations/accuweather/",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["accuweather"],

View File

@@ -25,6 +25,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import AccuWeatherDataUpdateCoordinator
@@ -49,7 +50,7 @@ PARALLEL_UPDATES = 1
class AccuWeatherSensorDescriptionMixin:
"""Mixin for AccuWeather sensor."""
value_fn: Callable[[dict[str, Any]], str | int | float | None]
value_fn: Callable[[dict[str, Any]], StateType]
@dataclass
@@ -58,281 +59,194 @@ class AccuWeatherSensorDescription(
):
"""Class describing AccuWeather sensor entities."""
attr_fn: Callable[[dict[str, Any]], dict[str, Any]] = lambda _: {}
day: int | None = None
attr_fn: Callable[[dict[str, Any]], dict[str, StateType]] = lambda _: {}
FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
*(
AccuWeatherSensorDescription(
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(
key="AirQuality",
icon="mdi:air-filter",
name="Air quality",
value_fn=lambda data: cast(str, data[ATTR_CATEGORY]),
device_class=SensorDeviceClass.ENUM,
options=["good", "hazardous", "high", "low", "moderate", "unhealthy"],
translation_key="air_quality",
),
*(
AccuWeatherSensorDescription(
key="CloudCoverDay",
icon="mdi:weather-cloudy",
entity_registry_enabled_default=False,
native_unit_of_measurement=PERCENTAGE,
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(
key="CloudCoverDay",
icon="mdi:weather-cloudy",
name="Cloud cover day",
entity_registry_enabled_default=False,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: cast(int, data),
),
*(
AccuWeatherSensorDescription(
key="CloudCoverNight",
icon="mdi:weather-cloudy",
entity_registry_enabled_default=False,
native_unit_of_measurement=PERCENTAGE,
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(
key="CloudCoverNight",
icon="mdi:weather-cloudy",
name="Cloud cover night",
entity_registry_enabled_default=False,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: cast(int, data),
),
*(
AccuWeatherSensorDescription(
key="Grass",
icon="mdi:grass",
entity_registry_enabled_default=False,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
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(
key="Grass",
icon="mdi:grass",
name="Grass pollen",
entity_registry_enabled_default=False,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="grass_pollen",
),
*(
AccuWeatherSensorDescription(
key="HoursOfSun",
icon="mdi:weather-partly-cloudy",
native_unit_of_measurement=UnitOfTime.HOURS,
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(
key="HoursOfSun",
icon="mdi:weather-partly-cloudy",
name="Hours of sun",
native_unit_of_measurement=UnitOfTime.HOURS,
value_fn=lambda data: cast(float, data),
),
*(
AccuWeatherSensorDescription(
key="LongPhraseDay",
value_fn=lambda data: cast(str, data),
translation_key=f"condition_day_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
AccuWeatherSensorDescription(
key="LongPhraseDay",
name="Condition day",
value_fn=lambda data: cast(str, data),
),
*(
AccuWeatherSensorDescription(
key="LongPhraseNight",
value_fn=lambda data: cast(str, data),
translation_key=f"condition_night_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
AccuWeatherSensorDescription(
key="LongPhraseNight",
name="Condition night",
value_fn=lambda data: cast(str, data),
),
*(
AccuWeatherSensorDescription(
key="Mold",
icon="mdi:blur",
entity_registry_enabled_default=False,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
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(
key="Mold",
icon="mdi:blur",
name="Mold pollen",
entity_registry_enabled_default=False,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="mold_pollen",
),
*(
AccuWeatherSensorDescription(
key="Ragweed",
icon="mdi:sprout",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
entity_registry_enabled_default=False,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
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(
key="Ragweed",
icon="mdi:sprout",
name="Ragweed pollen",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
entity_registry_enabled_default=False,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="ragweed_pollen",
),
*(
AccuWeatherSensorDescription(
key="RealFeelTemperatureMax",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
translation_key=f"realfeel_temperature_max_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
AccuWeatherSensorDescription(
key="RealFeelTemperatureMax",
device_class=SensorDeviceClass.TEMPERATURE,
name="RealFeel temperature max",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
),
*(
AccuWeatherSensorDescription(
key="RealFeelTemperatureMin",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
translation_key=f"realfeel_temperature_min_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
AccuWeatherSensorDescription(
key="RealFeelTemperatureMin",
device_class=SensorDeviceClass.TEMPERATURE,
name="RealFeel temperature min",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
),
*(
AccuWeatherSensorDescription(
key="RealFeelTemperatureShadeMax",
device_class=SensorDeviceClass.TEMPERATURE,
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
translation_key=f"realfeel_temperature_shade_max_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
AccuWeatherSensorDescription(
key="RealFeelTemperatureShadeMax",
device_class=SensorDeviceClass.TEMPERATURE,
name="RealFeel temperature shade max",
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
),
*(
AccuWeatherSensorDescription(
key="RealFeelTemperatureShadeMin",
device_class=SensorDeviceClass.TEMPERATURE,
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
translation_key=f"realfeel_temperature_shade_min_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
AccuWeatherSensorDescription(
key="RealFeelTemperatureShadeMin",
device_class=SensorDeviceClass.TEMPERATURE,
name="RealFeel temperature shade min",
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
),
*(
AccuWeatherSensorDescription(
key="SolarIrradianceDay",
icon="mdi:weather-sunny",
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
translation_key=f"solar_irradiance_day_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
AccuWeatherSensorDescription(
key="SolarIrradianceDay",
icon="mdi:weather-sunny",
name="Solar irradiance day",
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
),
*(
AccuWeatherSensorDescription(
key="SolarIrradianceNight",
icon="mdi:weather-sunny",
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
translation_key=f"solar_irradiance_night_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
AccuWeatherSensorDescription(
key="SolarIrradianceNight",
icon="mdi:weather-sunny",
name="Solar irradiance night",
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
),
*(
AccuWeatherSensorDescription(
key="ThunderstormProbabilityDay",
icon="mdi:weather-lightning",
native_unit_of_measurement=PERCENTAGE,
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(
key="ThunderstormProbabilityDay",
icon="mdi:weather-lightning",
name="Thunderstorm probability day",
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: cast(int, data),
),
*(
AccuWeatherSensorDescription(
key="ThunderstormProbabilityNight",
icon="mdi:weather-lightning",
native_unit_of_measurement=PERCENTAGE,
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(
key="ThunderstormProbabilityNight",
icon="mdi:weather-lightning",
name="Thunderstorm probability night",
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: cast(int, data),
),
*(
AccuWeatherSensorDescription(
key="Tree",
icon="mdi:tree-outline",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
entity_registry_enabled_default=False,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
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(
key="Tree",
icon="mdi:tree-outline",
name="Tree pollen",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER,
entity_registry_enabled_default=False,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="tree_pollen",
),
*(
AccuWeatherSensorDescription(
key="UVIndex",
icon="mdi:weather-sunny",
native_unit_of_measurement=UV_INDEX,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
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(
key="UVIndex",
icon="mdi:weather-sunny",
name="UV index",
native_unit_of_measurement=UV_INDEX,
value_fn=lambda data: cast(int, data[ATTR_VALUE]),
attr_fn=lambda data: {ATTR_LEVEL: data[ATTR_CATEGORY]},
translation_key="uv_index",
),
*(
AccuWeatherSensorDescription(
key="WindGustDay",
device_class=SensorDeviceClass.WIND_SPEED,
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
translation_key=f"wind_gust_speed_day_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
AccuWeatherSensorDescription(
key="WindGustDay",
device_class=SensorDeviceClass.WIND_SPEED,
name="Wind gust day",
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
),
*(
AccuWeatherSensorDescription(
key="WindGustNight",
device_class=SensorDeviceClass.WIND_SPEED,
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
translation_key=f"wind_gust_speed_night_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
AccuWeatherSensorDescription(
key="WindGustNight",
device_class=SensorDeviceClass.WIND_SPEED,
name="Wind gust night",
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
),
*(
AccuWeatherSensorDescription(
key="WindDay",
device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
translation_key=f"wind_speed_day_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
AccuWeatherSensorDescription(
key="WindDay",
device_class=SensorDeviceClass.WIND_SPEED,
name="Wind day",
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
),
*(
AccuWeatherSensorDescription(
key="WindNight",
device_class=SensorDeviceClass.WIND_SPEED,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
translation_key=f"wind_speed_night_{day}d",
day=day,
)
for day in range(MAX_FORECAST_DAYS + 1)
AccuWeatherSensorDescription(
key="WindNight",
device_class=SensorDeviceClass.WIND_SPEED,
name="Wind night",
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][ATTR_VALUE]),
attr_fn=lambda data: {"direction": data[ATTR_DIRECTION][ATTR_ENGLISH]},
),
)
@@ -340,117 +254,118 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
AccuWeatherSensorDescription(
key="ApparentTemperature",
device_class=SensorDeviceClass.TEMPERATURE,
name="Apparent temperature",
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="apparent_temperature",
),
AccuWeatherSensorDescription(
key="Ceiling",
device_class=SensorDeviceClass.DISTANCE,
icon="mdi:weather-fog",
name="Cloud ceiling",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfLength.METERS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
suggested_display_precision=0,
translation_key="cloud_ceiling",
),
AccuWeatherSensorDescription(
key="CloudCover",
icon="mdi:weather-cloudy",
name="Cloud cover",
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: cast(int, data),
translation_key="cloud_cover",
),
AccuWeatherSensorDescription(
key="DewPoint",
device_class=SensorDeviceClass.TEMPERATURE,
name="Dew point",
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="dew_point",
),
AccuWeatherSensorDescription(
key="RealFeelTemperature",
device_class=SensorDeviceClass.TEMPERATURE,
name="RealFeel temperature",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="realfeel_temperature",
),
AccuWeatherSensorDescription(
key="RealFeelTemperatureShade",
device_class=SensorDeviceClass.TEMPERATURE,
name="RealFeel temperature shade",
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="realfeel_temperature_shade",
),
AccuWeatherSensorDescription(
key="Precipitation",
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
name="Precipitation",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
attr_fn=lambda data: {"type": data["PrecipitationType"]},
translation_key="precipitation",
),
AccuWeatherSensorDescription(
key="PressureTendency",
device_class=SensorDeviceClass.ENUM,
icon="mdi:gauge",
name="Pressure tendency",
options=["falling", "rising", "steady"],
value_fn=lambda data: cast(str, data["LocalizedText"]).lower(),
translation_key="pressure_tendency",
value_fn=lambda data: cast(str, data["LocalizedText"]).lower(),
),
AccuWeatherSensorDescription(
key="UVIndex",
icon="mdi:weather-sunny",
name="UV index",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UV_INDEX,
value_fn=lambda data: cast(int, data),
attr_fn=lambda data: {ATTR_LEVEL: data["UVIndexText"]},
translation_key="uv_index",
),
AccuWeatherSensorDescription(
key="WetBulbTemperature",
device_class=SensorDeviceClass.TEMPERATURE,
name="Wet bulb temperature",
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="wet_bulb_temperature",
),
AccuWeatherSensorDescription(
key="WindChillTemperature",
device_class=SensorDeviceClass.TEMPERATURE,
name="Wind chill temperature",
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: cast(float, data[API_METRIC][ATTR_VALUE]),
translation_key="wind_chill_temperature",
),
AccuWeatherSensorDescription(
key="Wind",
device_class=SensorDeviceClass.WIND_SPEED,
name="Wind",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]),
translation_key="wind_speed",
),
AccuWeatherSensorDescription(
key="WindGust",
device_class=SensorDeviceClass.WIND_SPEED,
name="Wind gust",
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
value_fn=lambda data: cast(float, data[ATTR_SPEED][API_METRIC][ATTR_VALUE]),
translation_key="wind_gust_speed",
),
)
@@ -467,12 +382,14 @@ async def async_setup_entry(
]
if coordinator.forecast:
for description in FORECAST_SENSOR_TYPES:
# Some air quality/allergy sensors are only available for certain
# locations.
if description.key not in coordinator.data[ATTR_FORECAST][description.day]:
continue
sensors.append(AccuWeatherSensor(coordinator, description))
# Some air quality/allergy sensors are only available for certain
# locations.
sensors.extend(
AccuWeatherSensor(coordinator, description, forecast_day=day)
for day in range(MAX_FORECAST_DAYS + 1)
for description in FORECAST_SENSOR_TYPES
if description.key in coordinator.data[ATTR_FORECAST][0]
)
async_add_entities(sensors)
@@ -490,24 +407,28 @@ class AccuWeatherSensor(
self,
coordinator: AccuWeatherDataUpdateCoordinator,
description: AccuWeatherSensorDescription,
forecast_day: int | None = None,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self.forecast_day = description.day
self.entity_description = description
self._sensor_data = _get_sensor_data(
coordinator.data, description.key, self.forecast_day
coordinator.data, description.key, forecast_day
)
if self.forecast_day is not None:
self._attr_unique_id = f"{coordinator.location_key}-{description.key}-{self.forecast_day}".lower()
if forecast_day is not None:
self._attr_name = f"{description.name} {forecast_day}d"
self._attr_unique_id = (
f"{coordinator.location_key}-{description.key}-{forecast_day}".lower()
)
else:
self._attr_unique_id = (
f"{coordinator.location_key}-{description.key}".lower()
)
self._attr_device_info = coordinator.device_info
self.forecast_day = forecast_day
@property
def native_value(self) -> str | int | float | None:
def native_value(self) -> StateType:
"""Return the state."""
return self.entity_description.value_fn(self._sensor_data)

View File

@@ -24,8 +24,14 @@
},
"entity": {
"sensor": {
"air_quality_0d": {
"name": "Air quality today",
"pressure_tendency": {
"state": {
"steady": "Steady",
"rising": "Rising",
"falling": "Falling"
}
},
"air_quality": {
"state": {
"good": "Good",
"hazardous": "Hazardous",
@@ -35,761 +41,80 @@
"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": {
"name": "Apparent temperature"
},
"cloud_ceiling": {
"name": "Cloud ceiling"
},
"cloud_cover": {
"name": "Cloud cover"
},
"cloud_cover_day_0d": {
"name": "Cloud cover today"
},
"cloud_cover_day_1d": {
"name": "Cloud cover day 1"
},
"cloud_cover_day_2d": {
"name": "Cloud cover day 2"
},
"cloud_cover_day_3d": {
"name": "Cloud cover day 3"
},
"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": {
"name": "Dew point"
},
"grass_pollen_0d": {
"name": "Grass pollen today",
"grass_pollen": {
"state_attributes": {
"level": {
"name": "Level",
"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%]"
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
}
}
}
},
"grass_pollen_1d": {
"name": "Grass pollen day 1",
"mold_pollen": {
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"name": "Level",
"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%]"
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
}
}
}
},
"grass_pollen_2d": {
"name": "Grass pollen day 2",
"ragweed_pollen": {
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"name": "Level",
"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%]"
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
}
}
}
},
"grass_pollen_3d": {
"name": "Grass pollen day 3",
"tree_pollen": {
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"name": "Level",
"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%]"
}
}
}
},
"precipitation": {
"name": "[%key:component::sensor::entity_component::precipitation::name%]"
},
"pressure_tendency": {
"name": "Pressure tendency",
"state": {
"steady": "Steady",
"rising": "Rising",
"falling": "Falling"
}
},
"ragweed_pollen_0d": {
"name": "Ragweed 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%]"
}
}
}
},
"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%]"
}
}
}
},
"realfeel_temperature": {
"name": "RealFeel temperature"
},
"realfeel_temperature_max_0d": {
"name": "RealFeel temperature max today"
},
"realfeel_temperature_max_1d": {
"name": "RealFeel temperature max day 1"
},
"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": {
"name": "RealFeel temperature shade"
},
"realfeel_temperature_shade_max_0d": {
"name": "RealFeel temperature shade max today"
},
"realfeel_temperature_shade_max_1d": {
"name": "RealFeel temperature shade max day 1"
},
"realfeel_temperature_shade_max_2d": {
"name": "RealFeel temperature shade max day 2"
},
"realfeel_temperature_shade_max_3d": {
"name": "RealFeel temperature shade max day 3"
},
"realfeel_temperature_shade_max_4d": {
"name": "RealFeel temperature shade max day 4"
},
"realfeel_temperature_shade_min_0d": {
"name": "RealFeel temperature shade min today"
},
"realfeel_temperature_shade_min_1d": {
"name": "RealFeel temperature shade min day 1"
},
"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": {
"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_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%]"
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
}
}
}
},
"uv_index": {
"name": "UV index",
"state_attributes": {
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen_0d::state_attributes::level::name%]",
"name": "Level",
"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%]"
"good": "[%key:component::accuweather::entity::sensor::air_quality::state::good%]",
"hazardous": "[%key:component::accuweather::entity::sensor::air_quality::state::hazardous%]",
"high": "[%key:component::accuweather::entity::sensor::air_quality::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::air_quality::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::air_quality::state::moderate%]",
"unhealthy": "[%key:component::accuweather::entity::sensor::air_quality::state::unhealthy%]"
}
}
}
},
"uv_index_0d": {
"name": "UV index 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%]"
}
}
}
},
"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%]"
}
}
}
},
"wet_bulb_temperature": {
"name": "Wet bulb temperature"
},
"wind_speed": {
"name": "[%key:component::weather::entity_component::_::state_attributes::wind_speed::name%]"
},
"wind_chill_temperature": {
"name": "Wind chill temperature"
},
"wind_gust_speed": {
"name": "[%key:component::weather::entity_component::_::state_attributes::wind_gust_speed::name%]"
},
"wind_gust_speed_day_0d": {
"name": "Wind gust speed today"
},
"wind_gust_speed_day_1d": {
"name": "Wind gust speed day 1"
},
"wind_gust_speed_day_2d": {
"name": "Wind gust speed day 2"
},
"wind_gust_speed_day_3d": {
"name": "Wind gust speed day 3"
},
"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"
}
}
},

View File

@@ -14,11 +14,9 @@ from homeassistant.components.weather import (
ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME,
ATTR_FORECAST_UV_INDEX,
ATTR_FORECAST_WIND_BEARING,
Forecast,
SingleCoordinatorWeatherEntity,
WeatherEntityFeature,
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -28,8 +26,9 @@ from homeassistant.const import (
UnitOfSpeed,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utc_from_timestamp
from . import AccuWeatherDataUpdateCoordinator
@@ -40,7 +39,7 @@ from .const import (
ATTR_SPEED,
ATTR_VALUE,
ATTRIBUTION,
CONDITION_MAP,
CONDITION_CLASSES,
DOMAIN,
)
@@ -58,7 +57,7 @@ async def async_setup_entry(
class AccuWeatherEntity(
SingleCoordinatorWeatherEntity[AccuWeatherDataUpdateCoordinator]
CoordinatorEntity[AccuWeatherDataUpdateCoordinator], WeatherEntity
):
"""Define an AccuWeather entity."""
@@ -68,6 +67,9 @@ class AccuWeatherEntity(
def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None:
"""Initialize."""
super().__init__(coordinator)
# Coordinator data is used also for sensors which don't have units automatically
# converted, hence the weather entity's native units follow the configured unit
# system
self._attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
self._attr_native_pressure_unit = UnitOfPressure.HPA
self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS
@@ -76,13 +78,18 @@ class AccuWeatherEntity(
self._attr_unique_id = coordinator.location_key
self._attr_attribution = ATTRIBUTION
self._attr_device_info = coordinator.device_info
if self.coordinator.forecast:
self._attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
@property
def condition(self) -> str | None:
"""Return the current condition."""
return CONDITION_MAP.get(self.coordinator.data["WeatherIcon"])
try:
return [
k
for k, v in CONDITION_CLASSES.items()
if self.coordinator.data["WeatherIcon"] in v
][0]
except IndexError:
return None
@property
def cloud_coverage(self) -> float:
@@ -140,11 +147,6 @@ class AccuWeatherEntity(
"""Return the visibility."""
return cast(float, self.coordinator.data["Visibility"][API_METRIC][ATTR_VALUE])
@property
def uv_index(self) -> float:
"""Return the UV index."""
return cast(float, self.coordinator.data["UVIndex"])
@property
def forecast(self) -> list[Forecast] | None:
"""Return the forecast array."""
@@ -170,14 +172,10 @@ class AccuWeatherEntity(
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: item["WindGustDay"][ATTR_SPEED][
ATTR_VALUE
],
ATTR_FORECAST_UV_INDEX: item["UVIndex"][ATTR_VALUE],
ATTR_FORECAST_WIND_BEARING: item["WindDay"][ATTR_DIRECTION]["Degrees"],
ATTR_FORECAST_CONDITION: CONDITION_MAP.get(item["IconDay"]),
ATTR_FORECAST_CONDITION: [
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v
][0],
}
for item in self.coordinator.data[ATTR_FORECAST]
]
@callback
def _async_forecast_daily(self) -> list[Forecast] | None:
"""Return the daily forecast in native units."""
return self.forecast

View File

@@ -74,9 +74,9 @@ class AcmedaBase(entity.Entity):
return self.roller.id
@property
def device_info(self) -> dr.DeviceInfo:
def device_info(self) -> entity.DeviceInfo:
"""Return the device info."""
return dr.DeviceInfo(
return entity.DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
manufacturer="Rollease Acmeda",
name=self.roller.name,

View File

@@ -2,11 +2,11 @@
from __future__ import annotations
import asyncio
from asyncio import timeout
from contextlib import suppress
from typing import Any
import aiopulse
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
@@ -43,7 +43,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
hubs: list[aiopulse.Hub] = []
with suppress(asyncio.TimeoutError):
async with timeout(5):
async with async_timeout.timeout(5):
async for hub in aiopulse.Hub.discover():
if hub.id not in already_configured:
hubs.append(hub)

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
import logging
import telnetlib # pylint: disable=deprecated-module
import telnetlib
from typing import Final
import voluptuous as vol

View File

@@ -23,7 +23,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ACCOUNT_ID, CONNECTION_TYPE, DOMAIN, LOCAL

View File

@@ -4,8 +4,8 @@ from __future__ import annotations
from adguardhome import AdGuardHome, AdGuardHomeError
from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER

View File

@@ -1,43 +1,65 @@
add_url:
name: Add url
description: Add a new filter subscription to AdGuard Home.
fields:
name:
name: Name
description: The name of the filter subscription.
required: true
example: Example
selector:
text:
url:
name: Url
description: The filter URL to subscribe to, containing the filter rules.
required: true
example: https://www.example.com/filter/1.txt
selector:
text:
remove_url:
name: Remove url
description: Removes a filter subscription from AdGuard Home.
fields:
url:
name: Url
description: The filter subscription URL to remove.
required: true
example: https://www.example.com/filter/1.txt
selector:
text:
enable_url:
name: Enable url
description: Enables a filter subscription in AdGuard Home.
fields:
url:
name: Url
description: The filter subscription URL to enable.
required: true
example: https://www.example.com/filter/1.txt
selector:
text:
disable_url:
name: Disable url
description: Disables a filter subscription in AdGuard Home.
fields:
url:
name: Url
description: The filter subscription URL to disable.
required: true
example: https://www.example.com/filter/1.txt
selector:
text:
refresh:
name: Refresh
description: Refresh all filter subscriptions in AdGuard Home.
fields:
force:
name: Force
description: Force update (bypasses AdGuard Home throttling). "true" to force, or "false" to omit for a regular refresh.
default: false
selector:
boolean:

View File

@@ -72,61 +72,5 @@
"name": "Query log"
}
}
},
"services": {
"add_url": {
"name": "Add URL",
"description": "Add a new filter subscription to AdGuard Home.",
"fields": {
"name": {
"name": "[%key:common::config_flow::data::name%]",
"description": "The name of the filter subscription."
},
"url": {
"name": "[%key:common::config_flow::data::url%]",
"description": "The filter URL to subscribe to, containing the filter rules."
}
}
},
"remove_url": {
"name": "Remove URL",
"description": "Removes a filter subscription from AdGuard Home.",
"fields": {
"url": {
"name": "[%key:common::config_flow::data::url%]",
"description": "The filter subscription URL to remove."
}
}
},
"enable_url": {
"name": "Enable URL",
"description": "Enables a filter subscription in AdGuard Home.",
"fields": {
"url": {
"name": "[%key:common::config_flow::data::url%]",
"description": "The filter subscription URL to enable."
}
}
},
"disable_url": {
"name": "Disable URL",
"description": "Disables a filter subscription in AdGuard Home.",
"fields": {
"url": {
"name": "[%key:common::config_flow::data::url%]",
"description": "The filter subscription URL to disable."
}
}
},
"refresh": {
"name": "Refresh",
"description": "Refresh all filter subscriptions in AdGuard Home.",
"fields": {
"force": {
"name": "Force",
"description": "Force update (bypasses AdGuard Home throttling). \"true\" to force, or \"false\" to omit for a regular refresh."
}
}
}
}
}

View File

@@ -1,12 +1,12 @@
"""Support for Automation Device Specification (ADS)."""
import asyncio
from asyncio import timeout
from collections import namedtuple
import ctypes
import logging
import struct
import threading
import async_timeout
import pyads
import voluptuous as vol
@@ -301,7 +301,7 @@ class AdsEntity(Entity):
self._ads_hub.add_device_notification, ads_var, plctype, update
)
try:
async with timeout(10):
async with async_timeout.timeout(10):
await self._event.wait()
except asyncio.TimeoutError:
_LOGGER.debug("Variable %s: Timeout during first update", ads_var)

View File

@@ -1,13 +1,19 @@
# Describes the format for available ADS services
write_data_by_name:
name: Write data by name
description: Write a value to the connected ADS device.
fields:
adsvar:
name: ADS variable
description: The name of the variable to write to.
required: true
example: ".global_var"
selector:
text:
adstype:
name: ADS type
description: The data type of the variable to write to.
required: true
selector:
select:
@@ -19,6 +25,8 @@ write_data_by_name:
- "udint"
- "uint"
value:
name: Value
description: The value to write to the variable.
required: true
selector:
number:

View File

@@ -1,22 +0,0 @@
{
"services": {
"write_data_by_name": {
"name": "Write data by name",
"description": "Write a value to the connected ADS device.",
"fields": {
"adsvar": {
"name": "ADS variable",
"description": "The name of the variable to write to."
},
"adstype": {
"name": "ADS type",
"description": "The data type of the variable to write to."
},
"value": {
"name": "Value",
"description": "The value to write to the variable."
}
}
}
}
}

View File

@@ -125,13 +125,6 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
@property
def target_temperature(self) -> float | None:
"""Return the current target temperature."""
# If the system is in MyZone mode, and a zone is set, return that temperature instead.
if (
self._ac["myZone"] > 0
and not self._ac.get(ADVANTAGE_AIR_MYAUTO_ENABLED)
and not self._ac.get(ADVANTAGE_AIR_MYTEMP_ENABLED)
):
return self._myzone["setTemp"]
return self._ac["setTemp"]
@property

View File

@@ -4,7 +4,7 @@ from typing import Any
from advantage_air import ApiError
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
@@ -62,12 +62,6 @@ class AdvantageAirAcEntity(AdvantageAirEntity):
def _ac(self) -> dict[str, Any]:
return self.coordinator.data["aircons"][self.ac_key]["info"]
@property
def _myzone(self) -> dict[str, Any]:
return self.coordinator.data["aircons"][self.ac_key]["zones"].get(
f"z{self._ac['myZone']:02}"
)
class AdvantageAirZoneEntity(AdvantageAirAcEntity):
"""Parent class for Advantage Air Zone Entities."""

View File

@@ -4,7 +4,7 @@ from typing import Any
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN

View File

@@ -1,10 +1,14 @@
set_time_to:
name: Set Time To
description: Control timers to turn the system on or off after a set number of minutes
target:
entity:
integration: advantage_air
domain: sensor
fields:
minutes:
name: Minutes
description: Minutes until action
required: true
selector:
number:

View File

@@ -13,19 +13,7 @@
"port": "[%key:common::config_flow::data::port%]"
},
"description": "Connect to the API of your Advantage Air wall mounted tablet.",
"title": "[%key:common::action::connect%]"
}
}
},
"services": {
"set_time_to": {
"name": "Set time to",
"description": "Controls timers to turn the system on or off after a set number of minutes.",
"fields": {
"minutes": {
"name": "Minutes",
"description": "Minutes until action."
}
"title": "Connect"
}
}
}

View File

@@ -3,7 +3,7 @@
from homeassistant.components.update import UpdateEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN

View File

@@ -1,16 +1,11 @@
"""The AEMET OpenData component."""
import asyncio
import logging
from aemet_opendata.exceptions import TownNotFound
from aemet_opendata.interface import AEMET, ConnectionOptions
from aemet_opendata.interface import AEMET
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from .const import (
CONF_STATION_UPDATES,
@@ -32,17 +27,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
longitude = entry.data[CONF_LONGITUDE]
station_updates = entry.options.get(CONF_STATION_UPDATES, True)
options = ConnectionOptions(api_key, station_updates, True)
aemet = AEMET(aiohttp_client.async_get_clientsession(hass), options)
try:
await aemet.select_coordinates(latitude, longitude)
except TownNotFound as err:
_LOGGER.error(err)
return False
except asyncio.TimeoutError as err:
raise ConfigEntryNotReady("AEMET OpenData API timed out") from err
aemet = AEMET(api_key)
weather_coordinator = WeatherUpdateCoordinator(
hass, aemet, latitude, longitude, station_updates
)
weather_coordinator = WeatherUpdateCoordinator(hass, aemet)
await weather_coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})

View File

@@ -1,14 +1,13 @@
"""Config flow for AEMET OpenData."""
from __future__ import annotations
from aemet_opendata.exceptions import AuthError
from aemet_opendata.interface import AEMET, ConnectionOptions
from aemet_opendata import AEMET
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client, config_validation as cv
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.schema_config_entry_flow import (
SchemaFlowFormStep,
SchemaOptionsFlowHandler,
@@ -40,11 +39,8 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(f"{latitude}-{longitude}")
self._abort_if_unique_id_configured()
options = ConnectionOptions(user_input[CONF_API_KEY], False, True)
aemet = AEMET(aiohttp_client.async_get_clientsession(self.hass), options)
try:
await aemet.select_coordinates(latitude, longitude)
except AuthError:
api_online = await _is_aemet_api_online(self.hass, user_input[CONF_API_KEY])
if not api_online:
errors["base"] = "invalid_api_key"
if not errors:
@@ -74,3 +70,10 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> SchemaOptionsFlowHandler:
"""Get the options flow for this handler."""
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
async def _is_aemet_api_online(hass, api_key):
aemet = AEMET(api_key)
return await hass.async_add_executor_job(
aemet.get_conventional_observation_stations, False
)

View File

@@ -1,19 +1,6 @@
"""Constant values for the AEMET OpenData component."""
from __future__ import annotations
from aemet_opendata.const import (
AOD_COND_CLEAR_NIGHT,
AOD_COND_CLOUDY,
AOD_COND_FOG,
AOD_COND_LIGHTNING,
AOD_COND_LIGHTNING_RAINY,
AOD_COND_PARTLY_CLODUY,
AOD_COND_POURING,
AOD_COND_RAINY,
AOD_COND_SNOWY,
AOD_COND_SUNNY,
)
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY,
@@ -46,7 +33,6 @@ ATTR_API_FORECAST_TEMP = "temperature"
ATTR_API_FORECAST_TEMP_LOW = "templow"
ATTR_API_FORECAST_TIME = "datetime"
ATTR_API_FORECAST_WIND_BEARING = "wind_bearing"
ATTR_API_FORECAST_WIND_MAX_SPEED = "wind_max_speed"
ATTR_API_FORECAST_WIND_SPEED = "wind_speed"
ATTR_API_HUMIDITY = "humidity"
ATTR_API_PRESSURE = "pressure"
@@ -68,16 +54,94 @@ ATTR_API_WIND_MAX_SPEED = "wind-max-speed"
ATTR_API_WIND_SPEED = "wind-speed"
CONDITIONS_MAP = {
AOD_COND_CLEAR_NIGHT: ATTR_CONDITION_CLEAR_NIGHT,
AOD_COND_CLOUDY: ATTR_CONDITION_CLOUDY,
AOD_COND_FOG: ATTR_CONDITION_FOG,
AOD_COND_LIGHTNING: ATTR_CONDITION_LIGHTNING,
AOD_COND_LIGHTNING_RAINY: ATTR_CONDITION_LIGHTNING_RAINY,
AOD_COND_PARTLY_CLODUY: ATTR_CONDITION_PARTLYCLOUDY,
AOD_COND_POURING: ATTR_CONDITION_POURING,
AOD_COND_RAINY: ATTR_CONDITION_RAINY,
AOD_COND_SNOWY: ATTR_CONDITION_SNOWY,
AOD_COND_SUNNY: ATTR_CONDITION_SUNNY,
ATTR_CONDITION_CLEAR_NIGHT: {
"11n", # Despejado (de noche)
},
ATTR_CONDITION_CLOUDY: {
"14", # Nuboso
"14n", # Nuboso (de noche)
"15", # Muy nuboso
"15n", # Muy nuboso (de noche)
"16", # Cubierto
"16n", # Cubierto (de noche)
"17", # Nubes altas
"17n", # Nubes altas (de noche)
},
ATTR_CONDITION_FOG: {
"81", # Niebla
"81n", # Niebla (de noche)
"82", # Bruma - Neblina
"82n", # Bruma - Neblina (de noche)
},
ATTR_CONDITION_LIGHTNING: {
"51", # Intervalos nubosos con tormenta
"51n", # Intervalos nubosos con tormenta (de noche)
"52", # Nuboso con tormenta
"52n", # Nuboso con tormenta (de noche)
"53", # Muy nuboso con tormenta
"53n", # Muy nuboso con tormenta (de noche)
"54", # Cubierto con tormenta
"54n", # Cubierto con tormenta (de noche)
},
ATTR_CONDITION_LIGHTNING_RAINY: {
"61", # Intervalos nubosos con tormenta y lluvia escasa
"61n", # Intervalos nubosos con tormenta y lluvia escasa (de noche)
"62", # Nuboso con tormenta y lluvia escasa
"62n", # Nuboso con tormenta y lluvia escasa (de noche)
"63", # Muy nuboso con tormenta y lluvia escasa
"63n", # Muy nuboso con tormenta y lluvia escasa (de noche)
"64", # Cubierto con tormenta y lluvia escasa
"64n", # Cubierto con tormenta y lluvia escasa (de noche)
},
ATTR_CONDITION_PARTLYCLOUDY: {
"12", # Poco nuboso
"12n", # Poco nuboso (de noche)
"13", # Intervalos nubosos
"13n", # Intervalos nubosos (de noche)
},
ATTR_CONDITION_POURING: {
"27", # Chubascos
"27n", # Chubascos (de noche)
},
ATTR_CONDITION_RAINY: {
"23", # Intervalos nubosos con lluvia
"23n", # Intervalos nubosos con lluvia (de noche)
"24", # Nuboso con lluvia
"24n", # Nuboso con lluvia (de noche)
"25", # Muy nuboso con lluvia
"25n", # Muy nuboso con lluvia (de noche)
"26", # Cubierto con lluvia
"26n", # Cubierto con lluvia (de noche)
"43", # Intervalos nubosos con lluvia escasa
"43n", # Intervalos nubosos con lluvia escasa (de noche)
"44", # Nuboso con lluvia escasa
"44n", # Nuboso con lluvia escasa (de noche)
"45", # Muy nuboso con lluvia escasa
"45n", # Muy nuboso con lluvia escasa (de noche)
"46", # Cubierto con lluvia escasa
"46n", # Cubierto con lluvia escasa (de noche)
},
ATTR_CONDITION_SNOWY: {
"33", # Intervalos nubosos con nieve
"33n", # Intervalos nubosos con nieve (de noche)
"34", # Nuboso con nieve
"34n", # Nuboso con nieve (de noche)
"35", # Muy nuboso con nieve
"35n", # Muy nuboso con nieve (de noche)
"36", # Cubierto con nieve
"36n", # Cubierto con nieve (de noche)
"71", # Intervalos nubosos con nieve escasa
"71n", # Intervalos nubosos con nieve escasa (de noche)
"72", # Nuboso con nieve escasa
"72n", # Nuboso con nieve escasa (de noche)
"73", # Muy nuboso con nieve escasa
"73n", # Muy nuboso con nieve escasa (de noche)
"74", # Cubierto con nieve escasa
"74n", # Cubierto con nieve escasa (de noche)
},
ATTR_CONDITION_SUNNY: {
"11", # Despejado
},
}
FORECAST_MONITORED_CONDITIONS = [
@@ -122,3 +186,16 @@ FORECAST_MODE_ATTR_API = {
FORECAST_MODE_DAILY: ATTR_API_FORECAST_DAILY,
FORECAST_MODE_HOURLY: ATTR_API_FORECAST_HOURLY,
}
WIND_BEARING_MAP = {
"C": None,
"N": 0.0,
"NE": 45.0,
"E": 90.0,
"SE": 135.0,
"S": 180.0,
"SO": 225.0,
"O": 270.0,
"NO": 315.0,
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aemet",
"iot_class": "cloud_polling",
"loggers": ["aemet_opendata"],
"requirements": ["AEMET-OpenData==0.4.5"]
"requirements": ["AEMET-OpenData==0.2.2"]
}

View File

@@ -30,7 +30,6 @@ from .const import (
ATTR_API_FORECAST_TEMP_LOW,
ATTR_API_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_MAX_SPEED,
ATTR_API_FORECAST_WIND_SPEED,
ATTR_API_HUMIDITY,
ATTR_API_PRESSURE,
@@ -100,12 +99,6 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
name="Wind bearing",
native_unit_of_measurement=DEGREE,
),
SensorEntityDescription(
key=ATTR_API_FORECAST_WIND_MAX_SPEED,
name="Wind max speed",
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED,
),
SensorEntityDescription(
key=ATTR_API_FORECAST_WIND_SPEED,
name="Wind speed",
@@ -213,14 +206,13 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
name="Wind max speed",
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=ATTR_API_WIND_SPEED,
name="Wind speed",
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.WIND_SPEED,
),
)

View File

@@ -1,20 +1,14 @@
"""Support for the AEMET OpenData service."""
from typing import cast
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_PRECIPITATION,
ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
DOMAIN as WEATHER_DOMAIN,
Forecast,
SingleCoordinatorWeatherEntity,
WeatherEntityFeature,
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -23,9 +17,9 @@ from homeassistant.const import (
UnitOfSpeed,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
ATTR_API_CONDITION,
@@ -36,13 +30,11 @@ from .const import (
ATTR_API_FORECAST_TEMP_LOW,
ATTR_API_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_MAX_SPEED,
ATTR_API_FORECAST_WIND_SPEED,
ATTR_API_HUMIDITY,
ATTR_API_PRESSURE,
ATTR_API_TEMPERATURE,
ATTR_API_WIND_BEARING,
ATTR_API_WIND_MAX_SPEED,
ATTR_API_WIND_SPEED,
ATTRIBUTION,
DOMAIN,
@@ -72,7 +64,6 @@ FORECAST_MAP = {
ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP,
ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_MAX_SPEED: ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED,
},
}
@@ -88,33 +79,15 @@ async def async_setup_entry(
weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR]
entities = []
entity_registry = er.async_get(hass)
# Add daily + hourly entity for legacy config entries, only add daily for new
# config entries. This can be removed in HA Core 2024.3
if entity_registry.async_get_entity_id(
WEATHER_DOMAIN,
DOMAIN,
f"{config_entry.unique_id} {FORECAST_MODE_HOURLY}",
):
for mode in FORECAST_MODES:
name = f"{domain_data[ENTRY_NAME]} {mode}"
unique_id = f"{config_entry.unique_id} {mode}"
entities.append(AemetWeather(name, unique_id, weather_coordinator, mode))
else:
entities.append(
AemetWeather(
domain_data[ENTRY_NAME],
config_entry.unique_id,
weather_coordinator,
FORECAST_MODE_DAILY,
)
)
for mode in FORECAST_MODES:
name = f"{domain_data[ENTRY_NAME]} {mode}"
unique_id = f"{config_entry.unique_id} {mode}"
entities.append(AemetWeather(name, unique_id, weather_coordinator, mode))
async_add_entities(entities, False)
class AemetWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]):
class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity):
"""Implementation of an AEMET OpenData sensor."""
_attr_attribution = ATTRIBUTION
@@ -122,9 +95,6 @@ class AemetWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]):
_attr_native_pressure_unit = UnitOfPressure.HPA
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
_attr_supported_features = (
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
)
def __init__(
self,
@@ -147,32 +117,15 @@ class AemetWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]):
"""Return the current condition."""
return self.coordinator.data[ATTR_API_CONDITION]
def _forecast(self, forecast_mode: str) -> list[Forecast]:
"""Return the forecast array."""
forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[forecast_mode]]
forecast_map = FORECAST_MAP[forecast_mode]
return cast(
list[Forecast],
[
{ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()}
for forecast in forecasts
],
)
@property
def forecast(self) -> list[Forecast]:
def forecast(self):
"""Return the forecast array."""
return self._forecast(self._forecast_mode)
@callback
def _async_forecast_daily(self) -> list[Forecast]:
"""Return the daily forecast in native units."""
return self._forecast(FORECAST_MODE_DAILY)
@callback
def _async_forecast_hourly(self) -> list[Forecast]:
"""Return the hourly forecast in native units."""
return self._forecast(FORECAST_MODE_HOURLY)
forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]]
forecast_map = FORECAST_MAP[self._forecast_mode]
return [
{ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()}
for forecast in forecasts
]
@property
def humidity(self):
@@ -194,11 +147,6 @@ class AemetWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordinator]):
"""Return the wind bearing."""
return self.coordinator.data[ATTR_API_WIND_BEARING]
@property
def native_wind_gust_speed(self):
"""Return the wind gust speed in native units."""
return self.coordinator.data[ATTR_API_WIND_MAX_SPEED]
@property
def native_wind_speed(self):
"""Return the wind speed."""

View File

@@ -1,21 +1,22 @@
"""Weather data coordinator for the AEMET OpenData service."""
from __future__ import annotations
from asyncio import timeout
from dataclasses import dataclass, field
from datetime import timedelta
import logging
from typing import Any, Final
from aemet_opendata.const import (
AEMET_ATTR_DATE,
AEMET_ATTR_DAY,
AEMET_ATTR_DIRECTION,
AEMET_ATTR_ELABORATED,
AEMET_ATTR_FEEL_TEMPERATURE,
AEMET_ATTR_FORECAST,
AEMET_ATTR_HUMIDITY,
AEMET_ATTR_ID,
AEMET_ATTR_IDEMA,
AEMET_ATTR_MAX,
AEMET_ATTR_MIN,
AEMET_ATTR_NAME,
AEMET_ATTR_PRECIPITATION,
AEMET_ATTR_PRECIPITATION_PROBABILITY,
AEMET_ATTR_SKY_STATE,
@@ -24,25 +25,24 @@ from aemet_opendata.const import (
AEMET_ATTR_SPEED,
AEMET_ATTR_STATION_DATE,
AEMET_ATTR_STATION_HUMIDITY,
AEMET_ATTR_STATION_LOCATION,
AEMET_ATTR_STATION_PRESSURE,
AEMET_ATTR_STATION_PRESSURE_SEA,
AEMET_ATTR_STATION_TEMPERATURE,
AEMET_ATTR_STORM_PROBABILITY,
AEMET_ATTR_TEMPERATURE,
AEMET_ATTR_TEMPERATURE_FEELING,
AEMET_ATTR_WIND,
AEMET_ATTR_WIND_GUST,
ATTR_DATA,
)
from aemet_opendata.exceptions import AemetError
from aemet_opendata.forecast import ForecastValue
from aemet_opendata.helpers import (
get_forecast_day_value,
get_forecast_hour_value,
get_forecast_interval_value,
)
from aemet_opendata.interface import AEMET
import async_timeout
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
@@ -57,7 +57,6 @@ from .const import (
ATTR_API_FORECAST_TEMP_LOW,
ATTR_API_FORECAST_TIME,
ATTR_API_FORECAST_WIND_BEARING,
ATTR_API_FORECAST_WIND_MAX_SPEED,
ATTR_API_FORECAST_WIND_SPEED,
ATTR_API_HUMIDITY,
ATTR_API_PRESSURE,
@@ -79,19 +78,22 @@ from .const import (
ATTR_API_WIND_SPEED,
CONDITIONS_MAP,
DOMAIN,
WIND_BEARING_MAP,
)
_LOGGER = logging.getLogger(__name__)
API_TIMEOUT: Final[int] = 120
STATION_MAX_DELTA = timedelta(hours=2)
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
def format_condition(condition: str) -> str:
"""Return condition from dict CONDITIONS_MAP."""
val = ForecastValue.parse_condition(condition)
return CONDITIONS_MAP.get(val, val)
for key, value in CONDITIONS_MAP.items():
if condition in value:
return key
_LOGGER.error('Condition "%s" not found in CONDITIONS_MAP', condition)
return condition
def format_float(value) -> float | None:
@@ -110,33 +112,128 @@ def format_int(value) -> int | None:
return None
class TownNotFound(UpdateFailed):
"""Raised when town is not found."""
class WeatherUpdateCoordinator(DataUpdateCoordinator):
"""Weather data update coordinator."""
def __init__(
self,
hass: HomeAssistant,
aemet: AEMET,
) -> None:
def __init__(self, hass, aemet, latitude, longitude, station_updates):
"""Initialize coordinator."""
self.aemet = aemet
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=WEATHER_UPDATE_INTERVAL,
hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL
)
async def _async_update_data(self) -> dict[str, Any]:
"""Update coordinator data."""
async with timeout(API_TIMEOUT):
try:
await self.aemet.update()
except AemetError as error:
raise UpdateFailed(error) from error
weather_response = self.aemet.legacy_weather()
return self._convert_weather_response(weather_response)
self._aemet = aemet
self._station = None
self._town = None
self._latitude = latitude
self._longitude = longitude
self._station_updates = station_updates
self._data = {
"daily": None,
"hourly": None,
"station": None,
}
async def _async_update_data(self):
data = {}
async with async_timeout.timeout(120):
weather_response = await self._get_aemet_weather()
data = self._convert_weather_response(weather_response)
return data
async def _get_aemet_weather(self):
"""Poll weather data from AEMET OpenData."""
weather = await self.hass.async_add_executor_job(self._get_weather_and_forecast)
return weather
def _get_weather_station(self):
if not self._station:
self._station = (
self._aemet.get_conventional_observation_station_by_coordinates(
self._latitude, self._longitude
)
)
if self._station:
_LOGGER.debug(
"station found for coordinates [%s, %s]: %s",
self._latitude,
self._longitude,
self._station,
)
if not self._station:
_LOGGER.debug(
"station not found for coordinates [%s, %s]",
self._latitude,
self._longitude,
)
return self._station
def _get_weather_town(self):
if not self._town:
self._town = self._aemet.get_town_by_coordinates(
self._latitude, self._longitude
)
if self._town:
_LOGGER.debug(
"Town found for coordinates [%s, %s]: %s",
self._latitude,
self._longitude,
self._town,
)
if not self._town:
_LOGGER.error(
"Town not found for coordinates [%s, %s]",
self._latitude,
self._longitude,
)
raise TownNotFound
return self._town
def _get_weather_and_forecast(self):
"""Get weather and forecast data from AEMET OpenData."""
self._get_weather_town()
daily = self._aemet.get_specific_forecast_town_daily(self._town[AEMET_ATTR_ID])
if not daily:
_LOGGER.error(
'Error fetching daily data for town "%s"', self._town[AEMET_ATTR_ID]
)
hourly = self._aemet.get_specific_forecast_town_hourly(
self._town[AEMET_ATTR_ID]
)
if not hourly:
_LOGGER.error(
'Error fetching hourly data for town "%s"', self._town[AEMET_ATTR_ID]
)
station = None
if self._station_updates and self._get_weather_station():
station = self._aemet.get_conventional_observation_station_data(
self._station[AEMET_ATTR_IDEMA]
)
if not station:
_LOGGER.error(
'Error fetching data for station "%s"',
self._station[AEMET_ATTR_IDEMA],
)
if daily:
self._data["daily"] = daily
if hourly:
self._data["hourly"] = hourly
if station:
self._data["station"] = station
return AemetWeather(
self._data["daily"],
self._data["hourly"],
self._data["station"],
)
def _convert_weather_response(self, weather_response):
"""Format the weather response correctly."""
@@ -331,7 +428,6 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
),
ATTR_API_FORECAST_TEMP: self._get_temperature(day, hour),
ATTR_API_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(),
ATTR_API_FORECAST_WIND_MAX_SPEED: self._get_wind_max_speed(day, hour),
ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour),
ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour),
}
@@ -422,14 +518,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
def _get_station_id(self):
"""Get station ID from weather data."""
if self.aemet.station:
return self.aemet.station.get_id()
if self._station:
return self._station[AEMET_ATTR_IDEMA]
return None
def _get_station_name(self):
"""Get station name from weather data."""
if self.aemet.station:
return self.aemet.station.get_name()
if self._station:
return self._station[AEMET_ATTR_STATION_LOCATION]
return None
@staticmethod
@@ -465,19 +561,19 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
@staticmethod
def _get_temperature_feeling(day_data, hour):
"""Get temperature from weather data."""
val = get_forecast_hour_value(day_data[AEMET_ATTR_FEEL_TEMPERATURE], hour)
val = get_forecast_hour_value(day_data[AEMET_ATTR_TEMPERATURE_FEELING], hour)
return format_int(val)
def _get_town_id(self):
"""Get town ID from weather data."""
if self.aemet.town:
return self.aemet.town.get_id()
if self._town:
return self._town[AEMET_ATTR_ID]
return None
def _get_town_name(self):
"""Get town name from weather data."""
if self.aemet.town:
return self.aemet.town.get_name()
if self._town:
return self._town[AEMET_ATTR_NAME]
return None
@staticmethod
@@ -486,7 +582,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
val = get_forecast_hour_value(
day_data[AEMET_ATTR_WIND_GUST], hour, key=AEMET_ATTR_DIRECTION
)[0]
return ForecastValue.parse_wind_direction(val)
if val in WIND_BEARING_MAP:
return WIND_BEARING_MAP[val]
_LOGGER.error("%s not found in Wind Bearing map", val)
return None
@staticmethod
def _get_wind_bearing_day(day_data):
@@ -494,7 +593,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
val = get_forecast_day_value(
day_data[AEMET_ATTR_WIND], key=AEMET_ATTR_DIRECTION
)
return ForecastValue.parse_wind_direction(val)
if val in WIND_BEARING_MAP:
return WIND_BEARING_MAP[val]
_LOGGER.error("%s not found in Wind Bearing map", val)
return None
@staticmethod
def _get_wind_max_speed(day_data, hour):
@@ -521,3 +623,12 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
if val:
return format_int(val)
return None
@dataclass
class AemetWeather:
"""Class to harmonize weather data model."""
daily: dict = field(default_factory=dict)
hourly: dict = field(default_factory=dict)
station: dict = field(default_factory=dict)

View File

@@ -1,42 +1 @@
"""The AfterShip integration."""
from __future__ import annotations
from pyaftership import AfterShip, AfterShipException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up AfterShip from a config entry."""
hass.data.setdefault(DOMAIN, {})
session = async_get_clientsession(hass)
aftership = AfterShip(api_key=entry.data[CONF_API_KEY], session=session)
try:
await aftership.trackings.list()
except AfterShipException as err:
raise ConfigEntryNotReady from err
hass.data[DOMAIN][entry.entry_id] = aftership
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
"""The aftership component."""

View File

@@ -1,90 +0,0 @@
"""Config flow for AfterShip integration."""
from __future__ import annotations
import logging
from typing import Any
from pyaftership import AfterShip, AfterShipException
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class AfterShipConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for AfterShip."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match({CONF_API_KEY: user_input[CONF_API_KEY]})
try:
aftership = AfterShip(
api_key=user_input[CONF_API_KEY],
session=async_get_clientsession(self.hass),
)
await aftership.trackings.list()
except AfterShipException:
_LOGGER.exception("Aftership raised exception")
errors["base"] = "cannot_connect"
else:
return self.async_create_entry(title="AfterShip", data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
errors=errors,
)
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
"""Import configuration from yaml."""
try:
self._async_abort_entries_match({CONF_API_KEY: config[CONF_API_KEY]})
except AbortFlow as err:
async_create_issue(
self.hass,
DOMAIN,
"deprecated_yaml_import_issue_already_configured",
breaks_in_ha_version="2024.4.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml_import_issue_already_configured",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "AfterShip",
},
)
raise err
async_create_issue(
self.hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.4.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "AfterShip",
},
)
return self.async_create_entry(
title=config.get(CONF_NAME, "AfterShip"),
data={CONF_API_KEY: config[CONF_API_KEY]},
)

View File

@@ -2,7 +2,6 @@
"domain": "aftership",
"name": "AfterShip",
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aftership",
"iot_class": "cloud_polling",
"requirements": ["pyaftership==21.11.0"]

View File

@@ -11,7 +11,6 @@ from homeassistant.components.sensor import (
PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA,
SensorEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -21,7 +20,6 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_send,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import Throttle
@@ -60,43 +58,19 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the AfterShip sensor platform."""
aftership = AfterShip(
api_key=config[CONF_API_KEY], session=async_get_clientsession(hass)
)
apikey = config[CONF_API_KEY]
name = config[CONF_NAME]
session = async_get_clientsession(hass)
aftership = AfterShip(api_key=apikey, session=session)
try:
await aftership.trackings.list()
except AfterShipException:
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml_import_issue_cannot_connect",
breaks_in_ha_version="2024.4.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml_import_issue_cannot_connect",
translation_placeholders={
"integration_title": "AfterShip",
"url": "/config/integrations/dashboard/add?domain=aftership",
},
)
except AfterShipException as err:
_LOGGER.error("No tracking data found. Check API key is correct: %s", err)
return
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AfterShip sensor entities based on a config entry."""
aftership: AfterShip = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities([AfterShipSensor(aftership, config_entry.title)], True)
async_add_entities([AfterShipSensor(aftership, name)], True)
async def handle_add_tracking(call: ServiceCall) -> None:
"""Call when a user adds a new Aftership tracking from Home Assistant."""

View File

@@ -1,29 +1,43 @@
# Describes the format for available aftership services
add_tracking:
name: Add tracking
description: Add new tracking number to Aftership.
fields:
tracking_number:
name: Tracking number
description: Tracking number for the new tracking
required: true
example: "123456789"
selector:
text:
title:
name: Title
description: A custom title for the new tracking
example: "Laptop"
selector:
text:
slug:
name: Slug
description: Slug (carrier) of the new tracking
example: "USPS"
selector:
text:
remove_tracking:
name: Remove tracking
description: Remove a tracking number from Aftership.
fields:
tracking_number:
name: Tracking number
description: Tracking number of the tracking to remove
required: true
example: "123456789"
selector:
text:
slug:
name: Slug
description: Slug (carrier) of the tracking to remove
example: "USPS"
selector:
text:

View File

@@ -1,61 +0,0 @@
{
"config": {
"step": {
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"services": {
"add_tracking": {
"name": "Add tracking",
"description": "Adds a new tracking number to Aftership.",
"fields": {
"tracking_number": {
"name": "Tracking number",
"description": "Tracking number for the new tracking."
},
"title": {
"name": "Title",
"description": "A custom title for the new tracking."
},
"slug": {
"name": "Slug",
"description": "Slug (carrier) of the new tracking."
}
}
},
"remove_tracking": {
"name": "Remove tracking",
"description": "Removes a tracking number from Aftership.",
"fields": {
"tracking_number": {
"name": "[%key:component::aftership::services::add_tracking::fields::tracking_number::name%]",
"description": "Tracking number of the tracking to remove."
},
"slug": {
"name": "[%key:component::aftership::services::add_tracking::fields::slug::name%]",
"description": "Slug (carrier) of the tracking to remove."
}
}
}
},
"issues": {
"deprecated_yaml_import_issue_already_configured": {
"title": "The {integration_title} YAML configuration import failed",
"description": "Configuring {integration_title} using YAML is being removed but the YAML configuration was already imported.\n\nRemove the YAML configuration and restart Home Assistant."
},
"deprecated_yaml_import_issue_cannot_connect": {
"title": "The {integration_title} YAML configuration import failed",
"description": "Configuring {integration_title} using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
}
}
}

View File

@@ -13,7 +13,7 @@ from homeassistant.const import (
STATE_ALARM_DISARMED,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import CONNECTION, DOMAIN as AGENT_DOMAIN
@@ -47,16 +47,14 @@ class AgentBaseStation(AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_NIGHT
)
_attr_has_entity_name = True
_attr_name = None
def __init__(self, client):
"""Initialize the alarm control panel."""
self._client = client
self._attr_name = f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}"
self._attr_unique_id = f"{client.unique}_CP"
self._attr_device_info = DeviceInfo(
identifiers={(AGENT_DOMAIN, client.unique)},
name=f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}",
manufacturer="Agent",
model=CONST_ALARM_CONTROL_PANEL_NAME,
sw_version=client.version,

View File

@@ -8,7 +8,7 @@ from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
@@ -72,13 +72,12 @@ class AgentCamera(MjpegCamera):
_attr_attribution = ATTRIBUTION
_attr_should_poll = True # Cameras default to False
_attr_supported_features = CameraEntityFeature.ON_OFF
_attr_has_entity_name = True
_attr_name = None
def __init__(self, device):
"""Initialize as a subclass of MjpegCamera."""
self.device = device
self._removed = False
self._attr_name = f"{device.client.name} {device.name}"
self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}"
super().__init__(
name=device.name,
@@ -89,7 +88,7 @@ class AgentCamera(MjpegCamera):
identifiers={(AGENT_DOMAIN, self.unique_id)},
manufacturer="Agent",
model="Camera",
name=f"{device.client.name} {device.name}",
name=self.name,
sw_version=device.client.version,
)

View File

@@ -3,7 +3,7 @@
"name": "Agent DVR",
"codeowners": ["@ispysoftware"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
"documentation": "https://www.home-assistant.io/integrations/agent_dvr/",
"iot_class": "local_polling",
"loggers": ["agent"],
"requirements": ["agent-py==0.0.23"]

View File

@@ -1,28 +1,38 @@
start_recording:
name: Start recording
description: Enable continuous recording.
target:
entity:
integration: agent_dvr
domain: camera
stop_recording:
name: Stop recording
description: Disable continuous recording.
target:
entity:
integration: agent_dvr
domain: camera
enable_alerts:
name: Enable alerts
description: Enable alerts
target:
entity:
integration: agent_dvr
domain: camera
disable_alerts:
name: Disable alerts
description: Disable alerts
target:
entity:
integration: agent_dvr
domain: camera
snapshot:
name: Snapshot
description: Take a photo
target:
entity:
integration: agent_dvr

View File

@@ -16,27 +16,5 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"services": {
"start_recording": {
"name": "Start recording",
"description": "Enables continuous recording."
},
"stop_recording": {
"name": "Stop recording",
"description": "Disables continuous recording."
},
"enable_alerts": {
"name": "Enable alerts",
"description": "Enables alerts."
},
"disable_alerts": {
"name": "Disable alerts",
"description": "Disables alerts."
},
"snapshot": {
"name": "Snapshot",
"description": "Takes a photo."
}
}
}

View File

@@ -3,6 +3,13 @@ from __future__ import annotations
from datetime import timedelta
import logging
from math import ceil
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError
from airly import Airly
from airly.exceptions import AirlyError
import async_timeout
from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM
from homeassistant.config_entries import ConfigEntry
@@ -10,15 +17,53 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Pla
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import CONF_USE_NEAREST, DOMAIN, MIN_UPDATE_INTERVAL
from .coordinator import AirlyDataUpdateCoordinator
from .const import (
ATTR_API_ADVICE,
ATTR_API_CAQI,
ATTR_API_CAQI_DESCRIPTION,
ATTR_API_CAQI_LEVEL,
CONF_USE_NEAREST,
DOMAIN,
MAX_UPDATE_INTERVAL,
MIN_UPDATE_INTERVAL,
NO_AIRLY_SENSORS,
)
PLATFORMS = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
def set_update_interval(instances_count: int, requests_remaining: int) -> timedelta:
"""Return data update interval.
The number of requests is reset at midnight UTC so we calculate the update
interval based on number of minutes until midnight, the number of Airly instances
and the number of remaining requests.
"""
now = dt_util.utcnow()
midnight = dt_util.find_next_time_expression_time(
now, seconds=[0], minutes=[0], hours=[0]
)
minutes_to_midnight = (midnight - now).total_seconds() / 60
interval = timedelta(
minutes=min(
max(
ceil(minutes_to_midnight / requests_remaining * instances_count),
MIN_UPDATE_INTERVAL,
),
MAX_UPDATE_INTERVAL,
)
)
_LOGGER.debug("Data will be update every %s", interval)
return interval
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Airly as config entry."""
api_key = entry.data[CONF_API_KEY]
@@ -45,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
str(longitude),
),
):
device_entry = device_registry.async_get_device(identifiers={old_ids}) # type: ignore[arg-type]
device_entry = device_registry.async_get_device({old_ids}) # type: ignore[arg-type]
if device_entry and entry.entry_id in device_entry.config_entries:
new_ids = (DOMAIN, f"{latitude}-{longitude}")
device_registry.async_update_device(
@@ -86,3 +131,75 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
"""Define an object to hold Airly data."""
def __init__(
self,
hass: HomeAssistant,
session: ClientSession,
api_key: str,
latitude: float,
longitude: float,
update_interval: timedelta,
use_nearest: bool,
) -> None:
"""Initialize."""
self.latitude = latitude
self.longitude = longitude
# Currently, Airly only supports Polish and English
language = "pl" if hass.config.language == "pl" else "en"
self.airly = Airly(api_key, session, language=language)
self.use_nearest = use_nearest
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
async def _async_update_data(self) -> dict[str, str | float | int]:
"""Update data via library."""
data: dict[str, str | float | int] = {}
if self.use_nearest:
measurements = self.airly.create_measurements_session_nearest(
self.latitude, self.longitude, max_distance_km=5
)
else:
measurements = self.airly.create_measurements_session_point(
self.latitude, self.longitude
)
async with async_timeout.timeout(20):
try:
await measurements.update()
except (AirlyError, ClientConnectorError) as error:
raise UpdateFailed(error) from error
_LOGGER.debug(
"Requests remaining: %s/%s",
self.airly.requests_remaining,
self.airly.requests_per_day,
)
# Airly API sometimes returns None for requests remaining so we update
# update_interval only if we have valid value.
if self.airly.requests_remaining:
self.update_interval = set_update_interval(
len(self.hass.config_entries.async_entries(DOMAIN)),
self.airly.requests_remaining,
)
values = measurements.current["values"]
index = measurements.current["indexes"][0]
standards = measurements.current["standards"]
if index["description"] == NO_AIRLY_SENSORS:
raise UpdateFailed("Can't retrieve data: no Airly sensors in this area")
for value in values:
data[value["name"]] = value["value"]
for standard in standards:
data[f"{standard['pollutant']}_LIMIT"] = standard["limit"]
data[f"{standard['pollutant']}_PERCENT"] = standard["percent"]
data[ATTR_API_CAQI] = index["value"]
data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ")
data[ATTR_API_CAQI_DESCRIPTION] = index["description"]
data[ATTR_API_ADVICE] = index["advice"]
return data

View File

@@ -1,13 +1,13 @@
"""Adds config flow for Airly."""
from __future__ import annotations
from asyncio import timeout
from http import HTTPStatus
from typing import Any
from aiohttp import ClientSession
from airly import Airly
from airly.exceptions import AirlyError
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
@@ -105,7 +105,7 @@ async def test_location(
measurements = airly.create_measurements_session_point(
latitude=latitude, longitude=longitude
)
async with timeout(10):
async with async_timeout.timeout(10):
await measurements.update()
current = measurements.current

View File

@@ -1,126 +0,0 @@
"""DataUpdateCoordinator for the Airly integration."""
from asyncio import timeout
from datetime import timedelta
import logging
from math import ceil
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError
from airly import Airly
from airly.exceptions import AirlyError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import (
ATTR_API_ADVICE,
ATTR_API_CAQI,
ATTR_API_CAQI_DESCRIPTION,
ATTR_API_CAQI_LEVEL,
DOMAIN,
MAX_UPDATE_INTERVAL,
MIN_UPDATE_INTERVAL,
NO_AIRLY_SENSORS,
)
_LOGGER = logging.getLogger(__name__)
def set_update_interval(instances_count: int, requests_remaining: int) -> timedelta:
"""Return data update interval.
The number of requests is reset at midnight UTC so we calculate the update
interval based on number of minutes until midnight, the number of Airly instances
and the number of remaining requests.
"""
now = dt_util.utcnow()
midnight = dt_util.find_next_time_expression_time(
now, seconds=[0], minutes=[0], hours=[0]
)
minutes_to_midnight = (midnight - now).total_seconds() / 60
interval = timedelta(
minutes=min(
max(
ceil(minutes_to_midnight / requests_remaining * instances_count),
MIN_UPDATE_INTERVAL,
),
MAX_UPDATE_INTERVAL,
)
)
_LOGGER.debug("Data will be update every %s", interval)
return interval
class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
"""Define an object to hold Airly data."""
def __init__(
self,
hass: HomeAssistant,
session: ClientSession,
api_key: str,
latitude: float,
longitude: float,
update_interval: timedelta,
use_nearest: bool,
) -> None:
"""Initialize."""
self.latitude = latitude
self.longitude = longitude
# Currently, Airly only supports Polish and English
language = "pl" if hass.config.language == "pl" else "en"
self.airly = Airly(api_key, session, language=language)
self.use_nearest = use_nearest
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
async def _async_update_data(self) -> dict[str, str | float | int]:
"""Update data via library."""
data: dict[str, str | float | int] = {}
if self.use_nearest:
measurements = self.airly.create_measurements_session_nearest(
self.latitude, self.longitude, max_distance_km=5
)
else:
measurements = self.airly.create_measurements_session_point(
self.latitude, self.longitude
)
async with timeout(20):
try:
await measurements.update()
except (AirlyError, ClientConnectorError) as error:
raise UpdateFailed(error) from error
_LOGGER.debug(
"Requests remaining: %s/%s",
self.airly.requests_remaining,
self.airly.requests_per_day,
)
# Airly API sometimes returns None for requests remaining so we update
# update_interval only if we have valid value.
if self.airly.requests_remaining:
self.update_interval = set_update_interval(
len(self.hass.config_entries.async_entries(DOMAIN)),
self.airly.requests_remaining,
)
values = measurements.current["values"]
index = measurements.current["indexes"][0]
standards = measurements.current["standards"]
if index["description"] == NO_AIRLY_SENSORS:
raise UpdateFailed("Can't retrieve data: no Airly sensors in this area")
for value in values:
data[value["name"]] = value["value"]
for standard in standards:
data[f"{standard['pollutant']}_LIMIT"] = standard["limit"]
data[f"{standard['pollutant']}_PERCENT"] = standard["percent"]
data[ATTR_API_CAQI] = index["value"]
data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ")
data[ATTR_API_CAQI_DESCRIPTION] = index["description"]
data[ATTR_API_ADVICE] = index["advice"]
return data

View File

@@ -20,7 +20,8 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

View File

@@ -17,7 +17,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"wrong_location": "[%key:component::airly::config::error::wrong_location%]"
"wrong_location": "No Airly measuring stations in this area."
}
},
"system_health": {

View File

@@ -2,6 +2,11 @@
import datetime
import logging
from aiohttp.client_exceptions import ClientConnectorError
from pyairnow import WebServiceAPI
from pyairnow.conv import aqi_to_concentration
from pyairnow.errors import AirNowError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
@@ -12,9 +17,26 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
from .coordinator import AirNowDataUpdateCoordinator
from .const import (
ATTR_API_AQI,
ATTR_API_AQI_DESCRIPTION,
ATTR_API_AQI_LEVEL,
ATTR_API_AQI_PARAM,
ATTR_API_CAT_DESCRIPTION,
ATTR_API_CAT_LEVEL,
ATTR_API_CATEGORY,
ATTR_API_PM25,
ATTR_API_POLLUTANT,
ATTR_API_REPORT_DATE,
ATTR_API_REPORT_HOUR,
ATTR_API_STATE,
ATTR_API_STATION,
ATTR_API_STATION_LATITUDE,
ATTR_API_STATION_LONGITUDE,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR]
@@ -25,9 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api_key = entry.data[CONF_API_KEY]
latitude = entry.data[CONF_LATITUDE]
longitude = entry.data[CONF_LONGITUDE]
# Station Radius is a user-configurable option
distance = entry.options[CONF_RADIUS]
distance = entry.data[CONF_RADIUS]
# Reports are published hourly but update twice per hour
update_interval = datetime.timedelta(minutes=30)
@@ -45,33 +65,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
# Listen for option changes
entry.async_on_unload(entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", entry.version)
if entry.version == 1:
new_options = {CONF_RADIUS: entry.data[CONF_RADIUS]}
new_data = entry.data.copy()
del new_data[CONF_RADIUS]
entry.version = 2
hass.config_entries.async_update_entry(
entry, data=new_data, options=new_options
)
_LOGGER.info("Migration to version %s successful", entry.version)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -82,6 +80,70 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
class AirNowDataUpdateCoordinator(DataUpdateCoordinator):
"""Define an object to hold Airly data."""
def __init__(
self, hass, session, api_key, latitude, longitude, distance, update_interval
):
"""Initialize."""
self.latitude = latitude
self.longitude = longitude
self.distance = distance
self.airnow = WebServiceAPI(api_key, session=session)
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
async def _async_update_data(self):
"""Update data via library."""
data = {}
try:
obs = await self.airnow.observations.latLong(
self.latitude,
self.longitude,
distance=self.distance,
)
except (AirNowError, ClientConnectorError) as error:
raise UpdateFailed(error) from error
if not obs:
raise UpdateFailed("No data was returned from AirNow")
max_aqi = 0
max_aqi_level = 0
max_aqi_desc = ""
max_aqi_poll = ""
for obv in obs:
# Convert AQIs to Concentration
pollutant = obv[ATTR_API_AQI_PARAM]
concentration = aqi_to_concentration(obv[ATTR_API_AQI], pollutant)
data[obv[ATTR_API_AQI_PARAM]] = concentration
# Overall AQI is the max of all pollutant AQIs
if obv[ATTR_API_AQI] > max_aqi:
max_aqi = obv[ATTR_API_AQI]
max_aqi_level = obv[ATTR_API_CATEGORY][ATTR_API_CAT_LEVEL]
max_aqi_desc = obv[ATTR_API_CATEGORY][ATTR_API_CAT_DESCRIPTION]
max_aqi_poll = pollutant
# Copy other data from PM2.5 Value
if obv[ATTR_API_AQI_PARAM] == ATTR_API_PM25:
# Copy Report Details
data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE]
data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR]
# Copy Station Details
data[ATTR_API_STATE] = obv[ATTR_API_STATE]
data[ATTR_API_STATION] = obv[ATTR_API_STATION]
data[ATTR_API_STATION_LATITUDE] = obv[ATTR_API_STATION_LATITUDE]
data[ATTR_API_STATION_LONGITUDE] = obv[ATTR_API_STATION_LONGITUDE]
# Store Overall AQI
data[ATTR_API_AQI] = max_aqi
data[ATTR_API_AQI_LEVEL] = max_aqi_level
data[ATTR_API_AQI_DESCRIPTION] = max_aqi_desc
data[ATTR_API_POLLUTANT] = max_aqi_poll
return data

View File

@@ -1,12 +1,11 @@
"""Config flow for AirNow integration."""
import logging
from typing import Any
from pyairnow import WebServiceAPI
from pyairnow.errors import AirNowError, EmptyResponseError, InvalidKeyError
from pyairnow.errors import AirNowError, InvalidKeyError
import voluptuous as vol
from homeassistant import config_entries, core, data_entry_flow, exceptions
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
@@ -36,8 +35,6 @@ async def validate_input(hass: core.HomeAssistant, data):
raise InvalidAuth from exc
except AirNowError as exc:
raise CannotConnect from exc
except EmptyResponseError as exc:
raise InvalidLocation from exc
if not test_data:
raise InvalidLocation
@@ -49,7 +46,7 @@ async def validate_input(hass: core.HomeAssistant, data):
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for AirNow."""
VERSION = 2
VERSION = 1
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
@@ -76,14 +73,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
else:
# Create Entry
radius = user_input.pop(CONF_RADIUS)
return self.async_create_entry(
title=(
f"AirNow Sensor at {user_input[CONF_LATITUDE]},"
f" {user_input[CONF_LONGITUDE]}"
),
data=user_input,
options={CONF_RADIUS: radius},
)
return self.async_show_form(
@@ -97,49 +92,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
vol.Optional(
CONF_LONGITUDE, default=self.hass.config.longitude
): cv.longitude,
vol.Optional(CONF_RADIUS, default=150): vol.All(
int, vol.Range(min=5)
),
vol.Optional(CONF_RADIUS, default=150): int,
}
),
errors=errors,
)
@staticmethod
@core.callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Return the options flow."""
return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry):
"""Handle an options flow for AirNow."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> data_entry_flow.FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(data=user_input)
options_schema = vol.Schema(
{
vol.Optional(CONF_RADIUS): vol.All(
int,
vol.Range(min=5),
),
}
)
return self.async_show_form(
step_id="init",
data_schema=self.add_suggested_values_to_schema(
options_schema, self.config_entry.options
),
)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@@ -1,99 +0,0 @@
"""DataUpdateCoordinator for the AirNow integration."""
import logging
from aiohttp.client_exceptions import ClientConnectorError
from pyairnow import WebServiceAPI
from pyairnow.conv import aqi_to_concentration
from pyairnow.errors import AirNowError
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
ATTR_API_AQI,
ATTR_API_AQI_DESCRIPTION,
ATTR_API_AQI_LEVEL,
ATTR_API_AQI_PARAM,
ATTR_API_CAT_DESCRIPTION,
ATTR_API_CAT_LEVEL,
ATTR_API_CATEGORY,
ATTR_API_PM25,
ATTR_API_POLLUTANT,
ATTR_API_REPORT_DATE,
ATTR_API_REPORT_HOUR,
ATTR_API_STATE,
ATTR_API_STATION,
ATTR_API_STATION_LATITUDE,
ATTR_API_STATION_LONGITUDE,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
class AirNowDataUpdateCoordinator(DataUpdateCoordinator):
"""The AirNow update coordinator."""
def __init__(
self, hass, session, api_key, latitude, longitude, distance, update_interval
):
"""Initialize."""
self.latitude = latitude
self.longitude = longitude
self.distance = distance
self.airnow = WebServiceAPI(api_key, session=session)
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
async def _async_update_data(self):
"""Update data via library."""
data = {}
try:
obs = await self.airnow.observations.latLong(
self.latitude,
self.longitude,
distance=self.distance,
)
except (AirNowError, ClientConnectorError) as error:
raise UpdateFailed(error) from error
if not obs:
raise UpdateFailed("No data was returned from AirNow")
max_aqi = 0
max_aqi_level = 0
max_aqi_desc = ""
max_aqi_poll = ""
for obv in obs:
# Convert AQIs to Concentration
pollutant = obv[ATTR_API_AQI_PARAM]
concentration = aqi_to_concentration(obv[ATTR_API_AQI], pollutant)
data[obv[ATTR_API_AQI_PARAM]] = concentration
# Overall AQI is the max of all pollutant AQIs
if obv[ATTR_API_AQI] > max_aqi:
max_aqi = obv[ATTR_API_AQI]
max_aqi_level = obv[ATTR_API_CATEGORY][ATTR_API_CAT_LEVEL]
max_aqi_desc = obv[ATTR_API_CATEGORY][ATTR_API_CAT_DESCRIPTION]
max_aqi_poll = pollutant
# Copy other data from PM2.5 Value
if obv[ATTR_API_AQI_PARAM] == ATTR_API_PM25:
# Copy Report Details
data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE]
data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR]
# Copy Station Details
data[ATTR_API_STATE] = obv[ATTR_API_STATE]
data[ATTR_API_STATION] = obv[ATTR_API_STATION]
data[ATTR_API_STATION_LATITUDE] = obv[ATTR_API_STATION_LATITUDE]
data[ATTR_API_STATION_LONGITUDE] = obv[ATTR_API_STATION_LONGITUDE]
# Store Overall AQI
data[ATTR_API_AQI] = max_aqi
data[ATTR_API_AQI_LEVEL] = max_aqi_level
data[ATTR_API_AQI_DESCRIPTION] = max_aqi_desc
data[ATTR_API_POLLUTANT] = max_aqi_poll
return data

View File

@@ -17,7 +17,8 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -29,9 +30,6 @@ from .const import (
ATTR_API_AQI_LEVEL,
ATTR_API_O3,
ATTR_API_PM25,
ATTR_API_STATION,
ATTR_API_STATION_LATITUDE,
ATTR_API_STATION_LONGITUDE,
DEFAULT_NAME,
DOMAIN,
)
@@ -42,7 +40,6 @@ PARALLEL_UPDATES = 1
ATTR_DESCR = "description"
ATTR_LEVEL = "level"
ATTR_STATION = "reporting_station"
@dataclass
@@ -58,16 +55,6 @@ class AirNowEntityDescription(SensorEntityDescription, AirNowEntityDescriptionMi
"""Describes Airnow sensor entity."""
def station_extra_attrs(data: dict[str, Any]) -> dict[str, Any]:
"""Process extra attributes for station location (if available)."""
if ATTR_API_STATION in data:
return {
"lat": data.get(ATTR_API_STATION_LATITUDE),
"long": data.get(ATTR_API_STATION_LONGITUDE),
}
return {}
SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription(
key=ATTR_API_AQI,
@@ -98,13 +85,6 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
value_fn=lambda data: data.get(ATTR_API_O3),
extra_state_attributes_fn=None,
),
AirNowEntityDescription(
key=ATTR_API_STATION,
translation_key="station",
icon="mdi:blur",
value_fn=lambda data: data.get(ATTR_API_STATION),
extra_state_attributes_fn=station_extra_attrs,
),
)

View File

@@ -14,33 +14,17 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_location": "No results found for that location, try changing the location or station radius.",
"invalid_location": "No results found for that location",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
"step": {
"init": {
"data": {
"radius": "Station Radius (miles)"
}
}
}
},
"entity": {
"sensor": {
"o3": {
"name": "[%key:component::sensor::entity_component::ozone::name%]"
},
"station": {
"name": "PM2.5 reporting station",
"state_attributes": {
"lat": { "name": "[%key:common::config_flow::data::latitude%]" },
"long": { "name": "[%key:common::config_flow::data::longitude%]" }
}
}
}
}

View File

@@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL

View File

@@ -21,7 +21,7 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import (

View File

@@ -34,12 +34,8 @@ class Discovery:
def get_name(device: AirthingsDevice) -> str:
"""Generate name with model and identifier for device."""
name = device.friendly_name()
if identifier := device.identifier:
name += f" ({identifier})"
return name
"""Generate name with identifier for device."""
return f"{device.name} ({device.identifier})"
class AirthingsDeviceUpdateError(Exception):
@@ -160,7 +156,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="no_devices_found")
titles = {
address: discovery.device.name
address: get_name(discovery.device)
for (address, discovery) in self._discovered_devices.items()
}
return self.async_show_form(

View File

@@ -3,26 +3,13 @@
"name": "Airthings BLE",
"bluetooth": [
{
"manufacturer_id": 820,
"service_uuid": "b42e1f6e-ade7-11e4-89d3-123b93f75cba"
},
{
"manufacturer_id": 820,
"service_uuid": "b42e4a8e-ade7-11e4-89d3-123b93f75cba"
},
{
"manufacturer_id": 820,
"service_uuid": "b42e1c08-ade7-11e4-89d3-123b93f75cba"
},
{
"manufacturer_id": 820,
"service_uuid": "b42e3882-ade7-11e4-89d3-123b93f75cba"
"manufacturer_id": 820
}
],
"codeowners": ["@vincegio", "@LaStrada"],
"codeowners": ["@vincegio"],
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling",
"requirements": ["airthings-ble==0.5.6-2"]
"requirements": ["airthings-ble==0.5.3"]
}

View File

@@ -5,35 +5,26 @@ import logging
from airthings_ble import AirthingsDevice
from homeassistant import config_entries
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
EntityCategory,
Platform,
UnitOfPressure,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH,
DeviceInfo,
async_get as device_async_get,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_registry import (
RegistryEntry,
async_entries_for_device,
async_get as entity_async_get,
)
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
@@ -117,44 +108,9 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
}
@callback
def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None:
"""Migrate entities to new unique ids (with BLE Address)."""
ent_reg = entity_async_get(hass)
unique_id_trailer = f"_{sensor_name}"
new_unique_id = f"{address}{unique_id_trailer}"
if ent_reg.async_get_entity_id(DOMAIN, Platform.SENSOR, new_unique_id):
# New unique id already exists
return
dev_reg = device_async_get(hass)
if not (
device := dev_reg.async_get_device(
connections={(CONNECTION_BLUETOOTH, address)}
)
):
return
entities = async_entries_for_device(
ent_reg,
device_id=device.id,
include_disabled_entities=True,
)
matching_reg_entry: RegistryEntry | None = None
for entry in entities:
if entry.unique_id.endswith(unique_id_trailer) and (
not matching_reg_entry or "(" not in entry.unique_id
):
matching_reg_entry = entry
if not matching_reg_entry or matching_reg_entry.unique_id == new_unique_id:
# Already has the newest unique id format
return
entity_id = matching_reg_entry.entity_id
ent_reg.async_update_entity(entity_id=entity_id, new_unique_id=new_unique_id)
_LOGGER.debug("Migrated entity '%s' to unique id '%s'", entity_id, new_unique_id)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Airthings BLE sensors."""
@@ -182,7 +138,6 @@ async def async_setup_entry(
sensor_value,
)
continue
async_migrate(hass, coordinator.data.address, sensor_type)
entities.append(
AirthingsSensor(coordinator, coordinator.data, sensors_mapping[sensor_type])
)
@@ -207,11 +162,11 @@ class AirthingsSensor(
super().__init__(coordinator)
self.entity_description = entity_description
name = airthings_device.name
if identifier := airthings_device.identifier:
name += f" ({identifier})"
name = f"{airthings_device.name} {airthings_device.identifier}"
self._attr_unique_id = f"{airthings_device.address}_{entity_description.key}"
self._attr_unique_id = f"{name}_{entity_description.key}"
self._id = airthings_device.address
self._attr_device_info = DeviceInfo(
connections={
(
@@ -220,10 +175,9 @@ class AirthingsSensor(
)
},
name=name,
manufacturer=airthings_device.manufacturer,
manufacturer="Airthings",
hw_version=airthings_device.hw_version,
sw_version=airthings_device.sw_version,
model=airthings_device.model,
)
@property

View File

@@ -1,13 +1,19 @@
"""The AirTouch4 integration."""
from airtouch4pyapi import AirTouch
import logging
from airtouch4pyapi import AirTouch
from airtouch4pyapi.airtouch import AirTouchStatus
from homeassistant.components.climate import SCAN_INTERVAL
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
from .coordinator import AirtouchDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.CLIMATE]
@@ -38,3 +44,38 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class AirtouchDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Airtouch data."""
def __init__(self, hass, airtouch):
"""Initialize global Airtouch data updater."""
self.airtouch = airtouch
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
async def _async_update_data(self):
"""Fetch data from Airtouch."""
await self.airtouch.UpdateInfo()
if self.airtouch.Status != AirTouchStatus.OK:
raise UpdateFailed("Airtouch connection issue")
return {
"acs": [
{"ac_number": ac.AcNumber, "is_on": ac.IsOn}
for ac in self.airtouch.GetAcs()
],
"groups": [
{
"group_number": group.GroupNumber,
"group_name": group.GroupName,
"is_on": group.IsOn,
}
for group in self.airtouch.GetGroups()
],
}

View File

@@ -18,7 +18,7 @@ from homeassistant.components.climate import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -84,9 +84,6 @@ async def async_setup_entry(
class AirtouchAC(CoordinatorEntity, ClimateEntity):
"""Representation of an AirTouch 4 ac."""
_attr_has_entity_name = True
_attr_name = None
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
)
@@ -98,25 +95,38 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
self._ac_number = ac_number
self._airtouch = coordinator.airtouch
self._info = info
self._unit = self._airtouch.GetAcs()[ac_number]
self._attr_unique_id = f"ac_{ac_number}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"ac_{ac_number}")},
name=f"AC {ac_number}",
manufacturer="Airtouch",
model="Airtouch 4",
)
self._unit = self._airtouch.GetAcs()[self._ac_number]
@callback
def _handle_coordinator_update(self):
self._unit = self._airtouch.GetAcs()[self._ac_number]
return super()._handle_coordinator_update()
@property
def device_info(self) -> DeviceInfo:
"""Return device info for this device."""
return DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
name=self.name,
manufacturer="Airtouch",
model="Airtouch 4",
)
@property
def unique_id(self):
"""Return unique ID for this device."""
return f"ac_{self._ac_number}"
@property
def current_temperature(self):
"""Return the current temperature."""
return self._unit.Temperature
@property
def name(self):
"""Return the name of the climate device."""
return f"AC {self._ac_number}"
@property
def fan_mode(self):
"""Return fan mode of the AC this group belongs to."""
@@ -190,8 +200,6 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
class AirtouchGroup(CoordinatorEntity, ClimateEntity):
"""Representation of an AirTouch 4 group."""
_attr_has_entity_name = True
_attr_name = None
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_hvac_modes = AT_GROUP_MODES
@@ -200,22 +208,30 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
"""Initialize the climate device."""
super().__init__(coordinator)
self._group_number = group_number
self._attr_unique_id = group_number
self._airtouch = coordinator.airtouch
self._info = info
self._unit = self._airtouch.GetGroupByGroupNumber(group_number)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, group_number)},
manufacturer="Airtouch",
model="Airtouch 4",
name=self._unit.GroupName,
)
self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number)
@callback
def _handle_coordinator_update(self):
self._unit = self._airtouch.GetGroupByGroupNumber(self._group_number)
return super()._handle_coordinator_update()
@property
def device_info(self) -> DeviceInfo:
"""Return device info for this device."""
return DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
manufacturer="Airtouch",
model="Airtouch 4",
name=self.name,
)
@property
def unique_id(self):
"""Return unique ID for this device."""
return self._group_number
@property
def min_temp(self):
"""Return Minimum Temperature for AC of this group."""
@@ -226,6 +242,11 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
"""Return Max Temperature for AC of this group."""
return self._airtouch.acs[self._unit.BelongsToAc].MaxSetpoint
@property
def name(self):
"""Return the name of the climate device."""
return self._unit.GroupName
@property
def current_temperature(self):
"""Return the current temperature."""

View File

@@ -1,46 +0,0 @@
"""DataUpdateCoordinator for the airtouch integration."""
import logging
from airtouch4pyapi.airtouch import AirTouchStatus
from homeassistant.components.climate import SCAN_INTERVAL
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class AirtouchDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Airtouch data."""
def __init__(self, hass, airtouch):
"""Initialize global Airtouch data updater."""
self.airtouch = airtouch
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
async def _async_update_data(self):
"""Fetch data from Airtouch."""
await self.airtouch.UpdateInfo()
if self.airtouch.Status != AirTouchStatus.OK:
raise UpdateFailed("Airtouch connection issue")
return {
"acs": [
{"ac_number": ac.AcNumber, "is_on": ac.IsOn}
for ac in self.airtouch.GetAcs()
],
"groups": [
{
"group_number": group.GroupNumber,
"group_name": group.GroupName,
"is_on": group.IsOn,
}
for group in self.airtouch.GetGroups()
],
}

View File

@@ -1,7 +1,7 @@
{
"domain": "airtouch4",
"name": "AirTouch 4",
"codeowners": ["@samsinnamon"],
"codeowners": ["@LonePurpleWolf"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airtouch4",
"iot_class": "local_polling",

View File

@@ -421,10 +421,8 @@ class AirVisualEntity(CoordinatorEntity):
self._entry = entry
self.entity_description = description
# pylint: disable-next=hass-missing-super-call
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
await super().async_added_to_hass()
@callback
def update() -> None:

View File

@@ -109,21 +109,19 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"airvisual_checked_api_keys_lock", asyncio.Lock()
)
if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS:
coro = cloud_api.air_quality.nearest_city()
error_schema = self.geography_coords_schema
error_step = "geography_by_coords"
else:
coro = cloud_api.air_quality.city(
user_input[CONF_CITY], user_input[CONF_STATE], user_input[CONF_COUNTRY]
)
error_schema = GEOGRAPHY_NAME_SCHEMA
error_step = "geography_by_name"
async with valid_keys_lock:
if user_input[CONF_API_KEY] not in valid_keys:
if integration_type == INTEGRATION_TYPE_GEOGRAPHY_COORDS:
coro = cloud_api.air_quality.nearest_city()
error_schema = self.geography_coords_schema
error_step = "geography_by_coords"
else:
coro = cloud_api.air_quality.city(
user_input[CONF_CITY],
user_input[CONF_STATE],
user_input[CONF_COUNTRY],
)
error_schema = GEOGRAPHY_NAME_SCHEMA
error_step = "geography_by_name"
try:
await coro
except (InvalidKeyError, KeyExpiredError, UnauthorizedError):

View File

@@ -8,5 +8,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pyairvisual", "pysmb"],
"requirements": ["pyairvisual==2023.08.1"]
"requirements": ["pyairvisual==2022.12.1"]
}

View File

@@ -11,7 +11,7 @@
}
},
"geography_by_name": {
"title": "[%key:component::airvisual::config::step::geography_by_coords::title%]",
"title": "Configure a Geography",
"description": "Use the AirVisual cloud API to monitor a city/state/country.",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
@@ -45,7 +45,7 @@
"options": {
"step": {
"init": {
"title": "[%key:component::airvisual::config::step::user::title%]",
"title": "Configure AirVisual",
"data": {
"show_on_map": "Show monitored geography on the map"
}

View File

@@ -23,8 +23,7 @@ from homeassistant.const import (
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
@@ -61,10 +60,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Get data from the device."""
try:
data = await node.async_get_latest_measurements()
data["history"] = {}
if data["settings"].get("follow_mode") == "device":
history = await node.async_get_history(include_trends=False)
data["history"] = history.get("measurements", [])[-1]
except InvalidAuthenticationError as err:
raise ConfigEntryAuthFailed("Invalid Samba password") from err
except NodeConnectionError as err:

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pyairvisual", "pysmb"],
"requirements": ["pyairvisual==2023.08.1"]
"requirements": ["pyairvisual==2022.12.1"]
}

View File

@@ -30,9 +30,7 @@ from .const import DOMAIN
class AirVisualProMeasurementKeyMixin:
"""Define an entity description mixin to include a measurement key."""
value_fn: Callable[
[dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]], float | int
]
value_fn: Callable[[dict[str, Any], dict[str, Any], dict[str, Any]], float | int]
@dataclass
@@ -45,81 +43,75 @@ class AirVisualProMeasurementDescription(
SENSOR_DESCRIPTIONS = (
AirVisualProMeasurementDescription(
key="air_quality_index",
name="Air quality index",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements, history: measurements[
value_fn=lambda settings, status, measurements: measurements[
async_get_aqi_locale(settings)
],
),
AirVisualProMeasurementDescription(
key="outdoor_air_quality_index",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements, history: int(
history.get(
f'Outdoor {"AQI(US)" if settings["is_aqi_usa"] else "AQI(CN)"}', -1
)
),
translation_key="outdoor_air_quality_index",
),
AirVisualProMeasurementDescription(
key="battery_level",
name="Battery",
device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda settings, status, measurements, history: status["battery"],
value_fn=lambda settings, status, measurements: status["battery"],
),
AirVisualProMeasurementDescription(
key="carbon_dioxide",
name="C02",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements, history: measurements["co2"],
value_fn=lambda settings, status, measurements: measurements["co2"],
),
AirVisualProMeasurementDescription(
key="humidity",
name="Humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda settings, status, measurements, history: measurements[
"humidity"
],
value_fn=lambda settings, status, measurements: measurements["humidity"],
),
AirVisualProMeasurementDescription(
key="particulate_matter_0_1",
translation_key="pm01",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements, history: measurements["pm0_1"],
),
AirVisualProMeasurementDescription(
key="particulate_matter_1_0",
name="PM 0.1",
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements, history: measurements["pm1_0"],
value_fn=lambda settings, status, measurements: measurements["pm0_1"],
),
AirVisualProMeasurementDescription(
key="particulate_matter_1_0",
name="PM 1.0",
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements: measurements["pm1_0"],
),
AirVisualProMeasurementDescription(
key="particulate_matter_2_5",
name="PM 2.5",
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements, history: measurements["pm2_5"],
value_fn=lambda settings, status, measurements: measurements["pm2_5"],
),
AirVisualProMeasurementDescription(
key="temperature",
name="Temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements, history: measurements[
"temperature_C"
],
value_fn=lambda settings, status, measurements: measurements["temperature_C"],
),
AirVisualProMeasurementDescription(
key="voc",
name="VOC",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda settings, status, measurements, history: measurements["voc"],
value_fn=lambda settings, status, measurements: measurements["voc"],
),
)
@@ -158,5 +150,4 @@ class AirVisualProSensor(AirVisualProEntity, SensorEntity):
self.coordinator.data["settings"],
self.coordinator.data["status"],
self.coordinator.data["measurements"],
self.coordinator.data["history"],
)

View File

@@ -24,15 +24,5 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"entity": {
"sensor": {
"pm01": {
"name": "PM0.1"
},
"outdoor_air_quality_index": {
"name": "Outdoor air quality index"
}
}
}
}

View File

@@ -24,7 +24,6 @@ PLATFORMS: list[Platform] = [
Platform.CLIMATE,
Platform.SELECT,
Platform.SENSOR,
Platform.WATER_HEATER,
]
_LOGGER = logging.getLogger(__name__)

View File

@@ -217,8 +217,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
if ATTR_TEMPERATURE in kwargs:
params[API_SET_POINT] = kwargs[ATTR_TEMPERATURE]
if ATTR_TARGET_TEMP_LOW in kwargs and ATTR_TARGET_TEMP_HIGH in kwargs:
params[API_COOL_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH]
params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_LOW]
params[API_COOL_SET_POINT] = kwargs[ATTR_TARGET_TEMP_LOW]
params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH]
await self._async_update_hvac_params(params)
@callback
@@ -248,8 +248,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
self._attr_target_temperature_high = self.get_airzone_value(
AZD_COOL_TEMP_SET
)
self._attr_target_temperature_low = self.get_airzone_value(
AZD_HEAT_TEMP_SET
)
self._attr_target_temperature_low = self.get_airzone_value(
AZD_COOL_TEMP_SET
)

View File

@@ -1,13 +1,13 @@
"""The Airzone integration."""
from __future__ import annotations
from asyncio import timeout
from datetime import timedelta
import logging
from typing import Any
from aioairzone.exceptions import AirzoneError
from aioairzone.localapi import AirzoneLocalApi
import async_timeout
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -35,7 +35,7 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
async with timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC):
async with async_timeout.timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC):
try:
await self.airzone.update()
except AirzoneError as error:

View File

@@ -10,7 +10,6 @@ from aioairzone.const import (
AZD_AVAILABLE,
AZD_FIRMWARE,
AZD_FULL_NAME,
AZD_HOT_WATER,
AZD_ID,
AZD_MAC,
AZD_MODEL,
@@ -27,7 +26,7 @@ from aioairzone.exceptions import AirzoneError
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER
@@ -82,47 +81,6 @@ class AirzoneSystemEntity(AirzoneEntity):
return value
class AirzoneHotWaterEntity(AirzoneEntity):
"""Define an Airzone Hot Water entity."""
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
entry: ConfigEntry,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{entry.entry_id}_dhw")},
manufacturer=MANUFACTURER,
model="DHW",
name=self.get_airzone_value(AZD_NAME),
via_device=(DOMAIN, f"{entry.entry_id}_ws"),
)
self._attr_unique_id = entry.unique_id or entry.entry_id
def get_airzone_value(self, key: str) -> Any:
"""Return DHW value by key."""
return self.coordinator.data[AZD_HOT_WATER].get(key)
async def _async_update_dhw_params(self, params: dict[str, Any]) -> None:
"""Send DHW parameters to API."""
_params = {
API_SYSTEM_ID: 0,
**params,
}
_LOGGER.debug("update_dhw_params=%s", _params)
try:
await self.coordinator.airzone.set_dhw_parameters(_params)
except AirzoneError as error:
raise HomeAssistantError(
f"Failed to set dhw {self.name}: {error}"
) from error
self.coordinator.async_set_updated_data(self.coordinator.airzone.data())
class AirzoneWebServerEntity(AirzoneEntity):
"""Define an Airzone WebServer entity."""

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