mirror of
https://github.com/home-assistant/core.git
synced 2025-11-08 18:39:30 +00:00
Compare commits
3 Commits
2023.9.1
...
cached_pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd15059cfb | ||
|
|
c71768445b | ||
|
|
96a276a30c |
@@ -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/**
|
||||
|
||||
98
.coveragerc
98
.coveragerc
@@ -57,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/*
|
||||
@@ -83,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
|
||||
@@ -93,7 +91,6 @@ omit =
|
||||
homeassistant/components/aurora/__init__.py
|
||||
homeassistant/components/aurora/binary_sensor.py
|
||||
homeassistant/components/aurora/coordinator.py
|
||||
homeassistant/components/aurora/entity.py
|
||||
homeassistant/components/aurora/sensor.py
|
||||
homeassistant/components/avea/light.py
|
||||
homeassistant/components/avion/light.py
|
||||
@@ -126,6 +123,7 @@ omit =
|
||||
homeassistant/components/bluetooth_tracker/*
|
||||
homeassistant/components/bmw_connected_drive/__init__.py
|
||||
homeassistant/components/bmw_connected_drive/binary_sensor.py
|
||||
homeassistant/components/bmw_connected_drive/button.py
|
||||
homeassistant/components/bmw_connected_drive/coordinator.py
|
||||
homeassistant/components/bmw_connected_drive/lock.py
|
||||
homeassistant/components/bmw_connected_drive/notify.py
|
||||
@@ -169,10 +167,6 @@ 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/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
|
||||
@@ -188,6 +182,7 @@ omit =
|
||||
homeassistant/components/crownstone/listeners.py
|
||||
homeassistant/components/cups/sensor.py
|
||||
homeassistant/components/currencylayer/sensor.py
|
||||
homeassistant/components/daikin/__init__.py
|
||||
homeassistant/components/daikin/climate.py
|
||||
homeassistant/components/daikin/sensor.py
|
||||
homeassistant/components/daikin/switch.py
|
||||
@@ -217,12 +212,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
|
||||
@@ -237,11 +230,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/dwd_weather_warnings/const.py
|
||||
homeassistant/components/dwd_weather_warnings/coordinator.py
|
||||
homeassistant/components/dwd_weather_warnings/sensor.py
|
||||
@@ -273,12 +261,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/sensor.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
|
||||
@@ -291,8 +273,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
|
||||
@@ -309,13 +290,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
|
||||
@@ -330,8 +305,15 @@ 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/button.py
|
||||
homeassistant/components/esphome/camera.py
|
||||
homeassistant/components/esphome/cover.py
|
||||
homeassistant/components/esphome/domain_data.py
|
||||
homeassistant/components/esphome/entry_data.py
|
||||
homeassistant/components/esphome/light.py
|
||||
homeassistant/components/esphome/switch.py
|
||||
homeassistant/components/etherscan/sensor.py
|
||||
homeassistant/components/eufy/*
|
||||
homeassistant/components/eufylife_ble/__init__.py
|
||||
@@ -339,18 +321,13 @@ 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
|
||||
@@ -382,13 +359,10 @@ omit =
|
||||
homeassistant/components/fitbit/*
|
||||
homeassistant/components/fivem/__init__.py
|
||||
homeassistant/components/fivem/binary_sensor.py
|
||||
homeassistant/components/fivem/coordinator.py
|
||||
homeassistant/components/fivem/entity.py
|
||||
homeassistant/components/fivem/sensor.py
|
||||
homeassistant/components/fixer/sensor.py
|
||||
homeassistant/components/fjaraskupan/__init__.py
|
||||
homeassistant/components/fjaraskupan/binary_sensor.py
|
||||
homeassistant/components/fjaraskupan/coordinator.py
|
||||
homeassistant/components/fjaraskupan/fan.py
|
||||
homeassistant/components/fjaraskupan/light.py
|
||||
homeassistant/components/fjaraskupan/number.py
|
||||
@@ -416,6 +390,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
|
||||
@@ -431,7 +406,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/*
|
||||
@@ -622,7 +596,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
|
||||
@@ -680,7 +653,6 @@ omit =
|
||||
homeassistant/components/lookin/light.py
|
||||
homeassistant/components/lookin/media_player.py
|
||||
homeassistant/components/lookin/sensor.py
|
||||
homeassistant/components/loqed/sensor.py
|
||||
homeassistant/components/luci/device_tracker.py
|
||||
homeassistant/components/luftdaten/sensor.py
|
||||
homeassistant/components/lupusec/*
|
||||
@@ -725,15 +697,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/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
|
||||
@@ -779,13 +752,12 @@ 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
|
||||
@@ -876,6 +848,7 @@ omit =
|
||||
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
|
||||
@@ -887,9 +860,6 @@ omit =
|
||||
homeassistant/components/openweathermap/sensor.py
|
||||
homeassistant/components/openweathermap/weather_update_coordinator.py
|
||||
homeassistant/components/opnsense/__init__.py
|
||||
homeassistant/components/opower/__init__.py
|
||||
homeassistant/components/opower/coordinator.py
|
||||
homeassistant/components/opower/sensor.py
|
||||
homeassistant/components/opnsense/device_tracker.py
|
||||
homeassistant/components/opple/light.py
|
||||
homeassistant/components/oru/*
|
||||
@@ -972,8 +942,6 @@ omit =
|
||||
homeassistant/components/pyload/sensor.py
|
||||
homeassistant/components/qbittorrent/__init__.py
|
||||
homeassistant/components/qbittorrent/sensor.py
|
||||
homeassistant/components/qnap/__init__.py
|
||||
homeassistant/components/qnap/coordinator.py
|
||||
homeassistant/components/qnap/sensor.py
|
||||
homeassistant/components/qrcode/image_processing.py
|
||||
homeassistant/components/quantum_gateway/device_tracker.py
|
||||
@@ -1005,7 +973,6 @@ omit =
|
||||
homeassistant/components/renson/const.py
|
||||
homeassistant/components/renson/entity.py
|
||||
homeassistant/components/renson/sensor.py
|
||||
homeassistant/components/renson/binary_sensor.py
|
||||
homeassistant/components/raspyrfm/*
|
||||
homeassistant/components/recollect_waste/sensor.py
|
||||
homeassistant/components/recorder/repack.py
|
||||
@@ -1022,7 +989,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
|
||||
@@ -1080,6 +1046,12 @@ omit =
|
||||
homeassistant/components/sense/__init__.py
|
||||
homeassistant/components/sense/binary_sensor.py
|
||||
homeassistant/components/sense/sensor.py
|
||||
homeassistant/components/senseme/__init__.py
|
||||
homeassistant/components/senseme/discovery.py
|
||||
homeassistant/components/senseme/entity.py
|
||||
homeassistant/components/senseme/fan.py
|
||||
homeassistant/components/senseme/light.py
|
||||
homeassistant/components/senseme/switch.py
|
||||
homeassistant/components/senz/__init__.py
|
||||
homeassistant/components/senz/api.py
|
||||
homeassistant/components/senz/climate.py
|
||||
@@ -1184,13 +1156,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
|
||||
@@ -1334,6 +1300,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
|
||||
@@ -1351,13 +1320,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
|
||||
@@ -1440,10 +1406,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/const.py
|
||||
homeassistant/components/vodafone_station/coordinator.py
|
||||
homeassistant/components/vodafone_station/device_tracker.py
|
||||
homeassistant/components/volkszaehler/sensor.py
|
||||
homeassistant/components/volumio/__init__.py
|
||||
homeassistant/components/volumio/browse_media.py
|
||||
@@ -1513,6 +1475,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
|
||||
@@ -1527,9 +1490,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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -59,15 +59,15 @@ body:
|
||||
attributes:
|
||||
label: Integration causing the issue
|
||||
description: >
|
||||
The name of the integration, for example Automation or Philips Hue.
|
||||
The name of the integration. For example: Automation, Philips Hue
|
||||
- type: input
|
||||
id: integration_link
|
||||
attributes:
|
||||
label: Link to integration documentation on our website
|
||||
placeholder: "https://www.home-assistant.io/integrations/..."
|
||||
description: |
|
||||
Providing a link [to the documentation][docs] helps us categorize the issue and might speed up the
|
||||
investigation by automatically informing a contributor, while also providing a useful reference for others.
|
||||
Providing a link [to the documentation][docs] helps us categorize the
|
||||
issue, while also providing a useful reference for others.
|
||||
|
||||
[docs]: https://www.home-assistant.io/integrations
|
||||
|
||||
|
||||
31
.github/workflows/builder.yml
vendored
31
.github/workflows/builder.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: core
|
||||
DEFAULT_PYTHON: "3.11"
|
||||
DEFAULT_PYTHON: "3.10"
|
||||
|
||||
jobs:
|
||||
init:
|
||||
@@ -24,12 +24,12 @@ jobs:
|
||||
publish: ${{ steps.version.outputs.publish }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.6.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@v3.6.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@v3.6.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 }}
|
||||
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2023.08.0
|
||||
uses: home-assistant/builder@2023.06.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -251,10 +251,9 @@ jobs:
|
||||
- raspberrypi4-64
|
||||
- tinker
|
||||
- yellow
|
||||
- green
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v3.5.3
|
||||
|
||||
- name: Set build additional args
|
||||
run: |
|
||||
@@ -275,7 +274,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2023.08.0
|
||||
uses: home-assistant/builder@2023.06.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -293,7 +292,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v3.5.3
|
||||
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master
|
||||
@@ -325,16 +324,12 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v3.5.3
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.1.1
|
||||
uses: sigstore/cosign-installer@v3.0.5
|
||||
with:
|
||||
cosign-release: "v2.0.2"
|
||||
|
||||
|
||||
174
.github/workflows/ci.yaml
vendored
174
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
@@ -36,9 +32,9 @@ env:
|
||||
CACHE_VERSION: 5
|
||||
PIP_CACHE_VERSION: 4
|
||||
MYPY_CACHE_VERSION: 4
|
||||
HA_SHORT_VERSION: 2023.9
|
||||
DEFAULT_PYTHON: "3.11"
|
||||
ALL_PYTHON_VERSIONS: "['3.11']"
|
||||
HA_SHORT_VERSION: 2023.7
|
||||
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
|
||||
@@ -83,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@v3.6.0
|
||||
uses: actions/checkout@v3.5.3
|
||||
- name: Generate partial Python venv restore key
|
||||
id: generate_python_cache_key
|
||||
run: >-
|
||||
@@ -132,7 +127,6 @@ jobs:
|
||||
test_group_count=10
|
||||
tests="[]"
|
||||
tests_glob=""
|
||||
skip_coverage=""
|
||||
|
||||
if [[ "${{ steps.integrations.outputs.changes }}" != "[]" ]];
|
||||
then
|
||||
@@ -182,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
|
||||
@@ -207,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
|
||||
@@ -220,10 +206,10 @@ jobs:
|
||||
- info
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.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
|
||||
@@ -265,9 +251,9 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.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 }}
|
||||
@@ -311,9 +297,9 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.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 }}
|
||||
@@ -360,9 +346,9 @@ jobs:
|
||||
- pre-commit
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.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 }}
|
||||
@@ -454,10 +440,10 @@ jobs:
|
||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.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
|
||||
@@ -506,10 +492,10 @@ 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 -e . --config-settings editable_mode=compat
|
||||
pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<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 .
|
||||
|
||||
hassfest:
|
||||
name: Check hassfest
|
||||
@@ -522,10 +508,10 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.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
|
||||
@@ -554,10 +540,10 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.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
|
||||
@@ -587,10 +573,10 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.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
|
||||
@@ -631,10 +617,10 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.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
|
||||
@@ -713,10 +699,10 @@ jobs:
|
||||
bluez \
|
||||
ffmpeg
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.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
|
||||
@@ -748,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 \
|
||||
@@ -769,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 }}
|
||||
@@ -865,10 +824,10 @@ jobs:
|
||||
ffmpeg \
|
||||
libmariadb-dev-compat
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.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
|
||||
@@ -903,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 \
|
||||
@@ -931,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
|
||||
@@ -989,10 +931,10 @@ jobs:
|
||||
ffmpeg \
|
||||
postgresql-server-dev-14
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.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
|
||||
@@ -1027,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 \
|
||||
@@ -1056,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
|
||||
@@ -1076,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
|
||||
@@ -1084,7 +1008,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v3.5.3
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
- name: Upload coverage to Codecov (full coverage)
|
||||
@@ -1095,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)
|
||||
@@ -1105,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
|
||||
|
||||
6
.github/workflows/translations.yml
vendored
6
.github/workflows/translations.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
- "**strings.json"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.11"
|
||||
DEFAULT_PYTHON: "3.10"
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
@@ -19,10 +19,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.6.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 }}
|
||||
|
||||
|
||||
196
.github/workflows/wheels.yml
vendored
196
.github/workflows/wheels.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
architectures: ${{ steps.info.outputs.architectures }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v3.5.3
|
||||
|
||||
- name: Get information
|
||||
id: info
|
||||
@@ -47,7 +47,10 @@ jobs:
|
||||
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true"
|
||||
echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true"
|
||||
echo "GRPC_PYTHON_DISABLE_LIBC_COMPATIBILITY=true"
|
||||
echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc"
|
||||
# GRPC on armv7 needs -lexecinfo (issue #56669) since home assistant installs
|
||||
# execinfo-dev when building wheels. The setuptools build setup does not have an option for
|
||||
# adding a single LDFLAG so copy all relevant linux flags here (as of 1.43.0)
|
||||
echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc -lexecinfo"
|
||||
|
||||
# Fix out of memory issues with rust
|
||||
echo "CARGO_NET_GIT_FETCH_WITH_CLI=true"
|
||||
@@ -80,11 +83,11 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
abi: ["cp311"]
|
||||
abi: ["cp310", "cp311"]
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v3.5.3
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v3
|
||||
@@ -110,7 +113,7 @@ jobs:
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements.txt"
|
||||
|
||||
integrations_cp311:
|
||||
integrations_cp310:
|
||||
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: init
|
||||
@@ -118,11 +121,11 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
abi: ["cp311"]
|
||||
abi: ["cp310"]
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.6.0
|
||||
uses: actions/checkout@v3.5.3
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v3
|
||||
@@ -134,30 +137,20 @@ jobs:
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: (Un)comment packages
|
||||
- name: Uncomment packages
|
||||
run: |
|
||||
requirement_files="requirements_all.txt requirements_diff.txt"
|
||||
for requirement_file in ${requirement_files}; do
|
||||
sed -i "s|# pybluez|pybluez|g" ${requirement_file}
|
||||
sed -i "s|# beacontools|beacontools|g" ${requirement_file}
|
||||
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
|
||||
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
|
||||
sed -i "s|# evdev|evdev|g" ${requirement_file}
|
||||
sed -i "s|# pycups|pycups|g" ${requirement_file}
|
||||
sed -i "s|# homekit|homekit|g" ${requirement_file}
|
||||
sed -i "s|# decora-wifi|decora-wifi|g" ${requirement_file}
|
||||
sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file}
|
||||
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
|
||||
|
||||
# Some packages are not buildable on armhf anymore
|
||||
if [ "${{ matrix.arch }}" = "armhf" ]; then
|
||||
|
||||
# Pandas has issues building on armhf, it is expected they
|
||||
# will drop the platform in the near future (they consider it
|
||||
# "flimsy" on 386). The following packages depend on pandas,
|
||||
# so we comment them out.
|
||||
sed -i "s|env-canada|# env-canada|g" ${requirement_file}
|
||||
sed -i "s|noaa-coops|# noaa-coops|g" ${requirement_file}
|
||||
sed -i "s|pyezviz|# pyezviz|g" ${requirement_file}
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" ${requirement_file}
|
||||
fi
|
||||
sed -i "s|# opencv-python-headless|opencv-python-headless|g" ${requirement_file}
|
||||
done
|
||||
|
||||
- name: Split requirements all
|
||||
@@ -174,6 +167,165 @@ jobs:
|
||||
echo "NPY_DISABLE_SVML=1" >> .env_file
|
||||
fi
|
||||
|
||||
(
|
||||
# cmake > 3.22.2 have issue on arm
|
||||
# Tested until 3.22.5
|
||||
echo "cmake==3.22.2"
|
||||
) >> homeassistant/package_constraints.txt
|
||||
|
||||
# Do not pin numpy in wheels building
|
||||
sed -i "/numpy/d" homeassistant/package_constraints.txt
|
||||
|
||||
- name: Build wheels (part 1)
|
||||
uses: home-assistant/wheels@2023.04.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
arch: ${{ matrix.arch }}
|
||||
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;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.04.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
arch: ${{ matrix.arch }}
|
||||
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;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.04.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
arch: ${{ matrix.arch }}
|
||||
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;grpcio;sqlalchemy;protobuf
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtac"
|
||||
|
||||
# Wheels building for the cp311 ABI is currently split
|
||||
# This is mainly until we have figured out to get all wheels built.
|
||||
# Without harming our current workflow.
|
||||
integrations_cp311:
|
||||
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: init
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
abi: ["cp311"]
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.3
|
||||
|
||||
- name: Write alternative env-file for cp311
|
||||
run: |
|
||||
(
|
||||
echo "GRPC_BUILD_WITH_BORING_SSL_ASM=false"
|
||||
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true"
|
||||
echo "GRPC_PYTHON_BUILD_WITH_CYTHON=true"
|
||||
echo "GRPC_PYTHON_DISABLE_LIBC_COMPATIBILITY=true"
|
||||
|
||||
# GRPC on armv7 needed -lexecinfo (issue #56669) since home assistant installed
|
||||
# execinfo-dev when building wheels. However, this package is no longer available
|
||||
# Alpine 3.17, which we use for the cp311 ABI, so the flag should no longer be needed.
|
||||
echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc" # -lexecinfo
|
||||
|
||||
# Fix out of memory issues with rust
|
||||
echo "CARGO_NET_GIT_FETCH_WITH_CLI=true"
|
||||
|
||||
# OpenCV headless installation
|
||||
echo "CI_BUILD=1"
|
||||
echo "ENABLE_HEADLESS=1"
|
||||
|
||||
# Use C-Extension for sqlalchemy
|
||||
echo "REQUIRE_SQLALCHEMY_CEXT=1"
|
||||
) > .env_file
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: (Un)comment packages
|
||||
run: |
|
||||
requirement_files="requirements_all.txt requirements_diff.txt"
|
||||
for requirement_file in ${requirement_files}; do
|
||||
|
||||
# PyBluez no longer compiles. Commented it out for now.
|
||||
# It need further cleanup down the line, as all machine images
|
||||
# try to install it.
|
||||
# sed -i "s|# pybluez|pybluez|g" ${requirement_file}
|
||||
|
||||
# beacontools requires PyBluez.
|
||||
# sed -i "s|# beacontools|beacontools|g" ${requirement_file}
|
||||
|
||||
# It doesn't build for some reason, so we skip it for now.
|
||||
# Bumping to the latest version (4.7.0.72) supporting Python 3.11
|
||||
# doesn't help. Reverted bump in #91871. There are 8 registered
|
||||
# instances using this integration according to analytics.
|
||||
# sed -i "s|# opencv-python-headless|opencv-python-headless|g" ${requirement_file}
|
||||
|
||||
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
|
||||
sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file}
|
||||
sed -i "s|# evdev|evdev|g" ${requirement_file}
|
||||
sed -i "s|# pycups|pycups|g" ${requirement_file}
|
||||
sed -i "s|# homekit|homekit|g" ${requirement_file}
|
||||
sed -i "s|# decora-wifi|decora-wifi|g" ${requirement_file}
|
||||
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
|
||||
|
||||
# Some packages are not buildable on armhf anymore
|
||||
if [ "${{ matrix.arch }}" = "armhf" ]; then
|
||||
|
||||
# Pandas has issues building on armhf, it is expected they
|
||||
# will drop the platform in the near future (they consider it
|
||||
# "flimsy" on 386). The following packages depend on pandas,
|
||||
# so we comment them out.
|
||||
sed -i "s|env-canada|# env-canada|g" ${requirement_file}
|
||||
sed -i "s|noaa-coops|# noaa-coops|g" ${requirement_file}
|
||||
sed -i "s|pyezviz|# pyezviz|g" ${requirement_file}
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" ${requirement_file}
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Split requirements all
|
||||
run: |
|
||||
# We split requirements all into two different files.
|
||||
# This is to prevent the build from running out of memory when
|
||||
# resolving packages on 32-bits systems (like armhf, armv7).
|
||||
|
||||
split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 3) requirements_all.txt requirements_all.txt
|
||||
|
||||
- name: Adjust build env
|
||||
run: |
|
||||
if [ "${{ matrix.arch }}" = "i386" ]; then
|
||||
echo "NPY_DISABLE_SVML=1" >> .env_file
|
||||
fi
|
||||
|
||||
# Probably not an issue anymore. Removing for now.
|
||||
# (
|
||||
# # cmake > 3.22.2 have issue on arm
|
||||
# # Tested until 3.22.5
|
||||
# echo "cmake==3.22.2"
|
||||
# ) >> homeassistant/package_constraints.txt
|
||||
|
||||
# Do not pin numpy in wheels building
|
||||
sed -i "/numpy/d" homeassistant/package_constraints.txt
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -67,7 +67,6 @@ htmlcov/
|
||||
test-reports/
|
||||
test-results.xml
|
||||
test-output.xml
|
||||
pytest-*.txt
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.285
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.272
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
- --fix
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 23.7.0
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
@@ -17,8 +17,8 @@ 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/
|
||||
@@ -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$
|
||||
|
||||
@@ -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.*
|
||||
@@ -104,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.*
|
||||
@@ -149,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.*
|
||||
@@ -184,7 +179,6 @@ homeassistant.components.imap.*
|
||||
homeassistant.components.input_button.*
|
||||
homeassistant.components.input_select.*
|
||||
homeassistant.components.integration.*
|
||||
homeassistant.components.ipp.*
|
||||
homeassistant.components.iqvia.*
|
||||
homeassistant.components.isy994.*
|
||||
homeassistant.components.jellyfin.*
|
||||
@@ -197,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,7 +207,6 @@ homeassistant.components.luftdaten.*
|
||||
homeassistant.components.mailbox.*
|
||||
homeassistant.components.mastodon.*
|
||||
homeassistant.components.matter.*
|
||||
homeassistant.components.media_extractor.*
|
||||
homeassistant.components.media_player.*
|
||||
homeassistant.components.media_source.*
|
||||
homeassistant.components.metoffice.*
|
||||
@@ -285,6 +277,7 @@ homeassistant.components.scene.*
|
||||
homeassistant.components.schedule.*
|
||||
homeassistant.components.scrape.*
|
||||
homeassistant.components.select.*
|
||||
homeassistant.components.senseme.*
|
||||
homeassistant.components.sensibo.*
|
||||
homeassistant.components.sensirion_ble.*
|
||||
homeassistant.components.sensor.*
|
||||
@@ -302,7 +295,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.*
|
||||
@@ -328,7 +320,6 @@ 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.*
|
||||
|
||||
77
CODEOWNERS
77
CODEOWNERS
@@ -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
|
||||
@@ -49,6 +51,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/airthings/ @danielhiversen
|
||||
/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
|
||||
@@ -191,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
|
||||
@@ -207,8 +211,6 @@ build.json @home-assistant/supervisor
|
||||
/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
|
||||
@@ -275,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
|
||||
@@ -293,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
|
||||
@@ -317,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
|
||||
@@ -341,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
|
||||
@@ -358,8 +360,6 @@ build.json @home-assistant/supervisor
|
||||
/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
|
||||
@@ -425,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
|
||||
@@ -521,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
|
||||
@@ -606,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
|
||||
@@ -671,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
|
||||
@@ -709,8 +703,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/logi_circle/ @evanjd
|
||||
/homeassistant/components/lookin/ @ANMalko @bdraco
|
||||
/tests/components/lookin/ @ANMalko @bdraco
|
||||
/homeassistant/components/loqed/ @mikewoudenberg
|
||||
/tests/components/loqed/ @mikewoudenberg
|
||||
/homeassistant/components/lovelace/ @home-assistant/frontend
|
||||
/tests/components/lovelace/ @home-assistant/frontend
|
||||
/homeassistant/components/luci/ @mzdrale
|
||||
@@ -729,7 +721,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/mazda/ @bdr99
|
||||
/homeassistant/components/meater/ @Sotolotl @emontnemery
|
||||
/tests/components/meater/ @Sotolotl @emontnemery
|
||||
/homeassistant/components/media_extractor/ @joostlek
|
||||
/homeassistant/components/media_player/ @home-assistant/core
|
||||
/tests/components/media_player/ @home-assistant/core
|
||||
/homeassistant/components/media_source/ @hunterjm
|
||||
@@ -752,6 +743,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
|
||||
@@ -895,7 +887,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
|
||||
@@ -904,8 +895,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/openweathermap/ @fabaff @freekode @nzapponi
|
||||
/homeassistant/components/opnsense/ @mtreinish
|
||||
/tests/components/opnsense/ @mtreinish
|
||||
/homeassistant/components/opower/ @tronikos
|
||||
/tests/components/opower/ @tronikos
|
||||
/homeassistant/components/oralb/ @bdraco @Lash-L
|
||||
/tests/components/oralb/ @bdraco @Lash-L
|
||||
/homeassistant/components/oru/ @bvlaicu
|
||||
@@ -923,8 +912,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
|
||||
@@ -983,7 +970,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/qld_bushfire/ @exxamalte
|
||||
/tests/components/qld_bushfire/ @exxamalte
|
||||
/homeassistant/components/qnap/ @disforw
|
||||
/tests/components/qnap/ @disforw
|
||||
/homeassistant/components/qnap_qsw/ @Noltari
|
||||
/tests/components/qnap_qsw/ @Noltari
|
||||
/homeassistant/components/quantum_gateway/ @cisasteelersfan
|
||||
@@ -1011,8 +997,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/rapt_ble/ @sairon
|
||||
/homeassistant/components/raspberry_pi/ @home-assistant/core
|
||||
/tests/components/raspberry_pi/ @home-assistant/core
|
||||
/homeassistant/components/rdw/ @frenck @joostlek
|
||||
/tests/components/rdw/ @frenck @joostlek
|
||||
/homeassistant/components/rdw/ @frenck
|
||||
/tests/components/rdw/ @frenck
|
||||
/homeassistant/components/recollect_waste/ @bachya
|
||||
/tests/components/recollect_waste/ @bachya
|
||||
/homeassistant/components/recorder/ @home-assistant/core
|
||||
@@ -1029,6 +1015,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
|
||||
@@ -1057,8 +1045,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/ @gabe565 @lanrat
|
||||
/tests/components/ruckus_unleashed/ @gabe565 @lanrat
|
||||
/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
|
||||
@@ -1076,11 +1064,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
|
||||
@@ -1093,6 +1079,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/select/ @home-assistant/core
|
||||
/homeassistant/components/sense/ @kbickar
|
||||
/tests/components/sense/ @kbickar
|
||||
/homeassistant/components/senseme/ @mikelawrence @bdraco
|
||||
/tests/components/senseme/ @mikelawrence @bdraco
|
||||
/homeassistant/components/sensibo/ @andrey-git @gjohansson-ST
|
||||
/tests/components/sensibo/ @andrey-git @gjohansson-ST
|
||||
/homeassistant/components/sensirion_ble/ @akx
|
||||
@@ -1227,8 +1215,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/ @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
|
||||
@@ -1299,8 +1287,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
|
||||
@@ -1368,8 +1354,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
|
||||
@@ -1380,8 +1364,6 @@ 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/ @andrey-git
|
||||
@@ -1445,7 +1427,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
|
||||
/homeassistant/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
|
||||
/tests/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
|
||||
/homeassistant/components/yeelightsunflower/ @lindsaymarkward
|
||||
|
||||
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.08.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.08.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.08.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.08.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.08.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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 164 KiB |
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"domain": "trafikverket",
|
||||
"name": "Trafikverket",
|
||||
"integrations": [
|
||||
"trafikverket_camera",
|
||||
"trafikverket_ferry",
|
||||
"trafikverket_train",
|
||||
"trafikverket_weatherstation"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "u_tec",
|
||||
"name": "U-tec",
|
||||
"iot_standards": ["zwave"]
|
||||
"integrations": ["ultraloq"]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -34,7 +34,6 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
|
||||
"""An alarm_control_panel implementation for Abode."""
|
||||
|
||||
_attr_icon = ICON
|
||||
_attr_name = None
|
||||
_attr_code_arm_required = False
|
||||
_attr_supported_features = (
|
||||
AlarmControlPanelEntityFeature.ARM_HOME
|
||||
@@ -69,7 +68,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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -39,7 +39,6 @@ class AbodeCamera(AbodeDevice, Camera):
|
||||
"""Representation of an Abode camera."""
|
||||
|
||||
_device: AbodeCam
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, data: AbodeSystem, device: AbodeDev, event: Event) -> None:
|
||||
"""Initialize the Abode device."""
|
||||
|
||||
@@ -29,7 +29,6 @@ class AbodeCover(AbodeDevice, CoverEntity):
|
||||
"""Representation of an Abode cover."""
|
||||
|
||||
_device: AbodeCV
|
||||
_attr_name = None
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool:
|
||||
|
||||
@@ -42,7 +42,6 @@ class AbodeLight(AbodeDevice, LightEntity):
|
||||
"""Representation of an Abode light."""
|
||||
|
||||
_device: AbodeLT
|
||||
_attr_name = None
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
|
||||
@@ -29,7 +29,6 @@ class AbodeLock(AbodeDevice, LockEntity):
|
||||
"""Representation of an Abode lock."""
|
||||
|
||||
_device: AbodeLK
|
||||
_attr_name = None
|
||||
|
||||
def lock(self, **kwargs: Any) -> None:
|
||||
"""Lock the device."""
|
||||
|
||||
@@ -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,33 @@ async def async_setup_entry(
|
||||
class AbodeSensor(AbodeDevice, SensorEntity):
|
||||
"""A sensor implementation for Abode devices."""
|
||||
|
||||
entity_description: AbodeSensorDescription
|
||||
_device: AbodeSense
|
||||
_attr_has_entity_name = True
|
||||
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ class AbodeSwitch(AbodeDevice, SwitchEntity):
|
||||
"""Representation of an Abode switch."""
|
||||
|
||||
_device: AbodeSW
|
||||
_attr_name = None
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the device."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,6 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
|
||||
_attr_target_temperature_step = PRECISION_WHOLE
|
||||
_attr_max_temp = 32
|
||||
_attr_min_temp = 16
|
||||
_attr_name = None
|
||||
|
||||
_attr_hvac_modes = [
|
||||
HVACMode.OFF,
|
||||
|
||||
@@ -9,30 +9,17 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
|
||||
|
||||
TO_REDACT = [
|
||||
"dealerPhoneNumber",
|
||||
"latitude",
|
||||
"logoPIN",
|
||||
"longitude",
|
||||
"postCode",
|
||||
"rid",
|
||||
"deviceNames",
|
||||
"deviceIds",
|
||||
"deviceIdsV2",
|
||||
"backupId",
|
||||
]
|
||||
TO_REDACT = ["dealerPhoneNumber", "latitude", "logoPIN", "longitude", "postCode"]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id].coordinator.data
|
||||
data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]["coordinator"].data
|
||||
|
||||
# Return only the relevant children
|
||||
return {
|
||||
"aircons": data.get("aircons"),
|
||||
"myLights": data.get("myLights"),
|
||||
"myThings": data.get("myThings"),
|
||||
"aircons": data["aircons"],
|
||||
"system": async_redact_data(data["system"], TO_REDACT),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -84,8 +84,6 @@ class AdvantageAirZoneEntity(AdvantageAirAcEntity):
|
||||
class AdvantageAirThingEntity(AdvantageAirEntity):
|
||||
"""Parent class for Advantage Air Things Entities."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, instance: AdvantageAirData, thing: dict[str, Any]) -> None:
|
||||
"""Initialize common aspects of an Advantage Air Things entity."""
|
||||
super().__init__(instance)
|
||||
|
||||
@@ -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
|
||||
@@ -41,7 +41,6 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity):
|
||||
"""Representation of Advantage Air Light."""
|
||||
|
||||
_attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, instance: AdvantageAirData, light: dict[str, Any]) -> None:
|
||||
"""Initialize an Advantage Air Light."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
"""The AEMET OpenData component."""
|
||||
|
||||
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.helpers import aiohttp_client
|
||||
|
||||
from .const import (
|
||||
CONF_STATION_UPDATES,
|
||||
@@ -30,15 +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)
|
||||
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
|
||||
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, {})
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
)
|
||||
|
||||
@@ -33,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"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aemet_opendata"],
|
||||
"requirements": ["AEMET-OpenData==0.4.4"]
|
||||
"requirements": ["AEMET-OpenData==0.2.2"]
|
||||
}
|
||||
|
||||
@@ -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,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,
|
||||
@@ -71,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,
|
||||
},
|
||||
}
|
||||
@@ -87,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
|
||||
@@ -121,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,
|
||||
@@ -146,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):
|
||||
|
||||
@@ -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,24 +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.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
|
||||
|
||||
@@ -56,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,
|
||||
@@ -83,7 +83,6 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_TIMEOUT: Final[int] = 120
|
||||
STATION_MAX_DELTA = timedelta(hours=2)
|
||||
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
@@ -113,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."""
|
||||
@@ -334,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),
|
||||
}
|
||||
@@ -425,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
|
||||
@@ -468,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
|
||||
@@ -530,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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""The Airly integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from math import ceil
|
||||
@@ -10,6 +9,7 @@ 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
|
||||
@@ -90,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(
|
||||
@@ -167,7 +167,7 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
measurements = self.airly.create_measurements_session_point(
|
||||
self.latitude, self.longitude
|
||||
)
|
||||
async with timeout(20):
|
||||
async with async_timeout.timeout(20):
|
||||
try:
|
||||
await measurements.update()
|
||||
except (AirlyError, ClientConnectorError) as error:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -47,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)
|
||||
@@ -67,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)
|
||||
@@ -104,11 +80,6 @@ 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."""
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
@@ -88,16 +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=lambda data: {
|
||||
"lat": data[ATTR_API_STATION_LATITUDE],
|
||||
"long": data[ATTR_API_STATION_LONGITUDE],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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%]" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -3,20 +3,7 @@
|
||||
"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"],
|
||||
@@ -24,5 +11,5 @@
|
||||
"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"]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH
|
||||
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 (
|
||||
@@ -161,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"{name}_{entity_description.key}"
|
||||
|
||||
self._id = airthings_device.address
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={
|
||||
(
|
||||
@@ -174,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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "airtouch4",
|
||||
"name": "AirTouch 4",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@LonePurpleWolf"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airtouch4",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyairvisual", "pysmb"],
|
||||
"requirements": ["pyairvisual==2023.08.1"]
|
||||
"requirements": ["pyairvisual==2022.12.1"]
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
}
|
||||
},
|
||||
"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%]",
|
||||
"city": "City",
|
||||
"country": "Country",
|
||||
"state": "State"
|
||||
"state": "state"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyairvisual", "pysmb"],
|
||||
"requirements": ["pyairvisual==2023.08.1"]
|
||||
"requirements": ["pyairvisual==2022.12.1"]
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,8 +193,6 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set hvac mode."""
|
||||
slave_raise = False
|
||||
|
||||
params = {}
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
params[API_ON] = 0
|
||||
@@ -204,13 +202,12 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
if self.get_airzone_value(AZD_MASTER):
|
||||
params[API_MODE] = mode
|
||||
else:
|
||||
slave_raise = True
|
||||
raise HomeAssistantError(
|
||||
f"Mode can't be changed on slave zone {self.name}"
|
||||
)
|
||||
params[API_ON] = 1
|
||||
await self._async_update_hvac_params(params)
|
||||
|
||||
if slave_raise:
|
||||
raise HomeAssistantError(f"Mode can't be changed on slave zone {self.name}")
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
params = {}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,31 +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)
|
||||
|
||||
|
||||
class AirzoneWebServerEntity(AirzoneEntity):
|
||||
"""Define an Airzone WebServer entity."""
|
||||
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.6.8"]
|
||||
"requirements": ["aioairzone==0.6.4"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
from typing import Any, Final
|
||||
|
||||
from aioairzone.const import (
|
||||
AZD_HOT_WATER,
|
||||
AZD_HUMIDITY,
|
||||
AZD_NAME,
|
||||
AZD_TEMP,
|
||||
@@ -32,21 +31,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
from .entity import (
|
||||
AirzoneEntity,
|
||||
AirzoneHotWaterEntity,
|
||||
AirzoneWebServerEntity,
|
||||
AirzoneZoneEntity,
|
||||
)
|
||||
|
||||
HOT_WATER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
SensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key=AZD_TEMP,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
from .entity import AirzoneEntity, AirzoneWebServerEntity, AirzoneZoneEntity
|
||||
|
||||
WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
SensorEntityDescription(
|
||||
@@ -86,18 +71,6 @@ async def async_setup_entry(
|
||||
|
||||
sensors: list[AirzoneSensor] = []
|
||||
|
||||
if AZD_HOT_WATER in coordinator.data:
|
||||
dhw_data = coordinator.data[AZD_HOT_WATER]
|
||||
for description in HOT_WATER_SENSOR_TYPES:
|
||||
if description.key in dhw_data:
|
||||
sensors.append(
|
||||
AirzoneHotWaterSensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
)
|
||||
)
|
||||
|
||||
if AZD_WEBSERVER in coordinator.data:
|
||||
ws_data = coordinator.data[AZD_WEBSERVER]
|
||||
for description in WEBSERVER_SENSOR_TYPES:
|
||||
@@ -141,30 +114,6 @@ class AirzoneSensor(AirzoneEntity, SensorEntity):
|
||||
self._attr_native_value = self.get_airzone_value(self.entity_description.key)
|
||||
|
||||
|
||||
class AirzoneHotWaterSensor(AirzoneHotWaterEntity, AirzoneSensor):
|
||||
"""Define an Airzone Hot Water sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, entry)
|
||||
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_dhw_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_native_unit_of_measurement = TEMP_UNIT_LIB_TO_HASS.get(
|
||||
self.get_airzone_value(AZD_TEMP_UNIT)
|
||||
)
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
|
||||
class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor):
|
||||
"""Define an Airzone WebServer sensor."""
|
||||
|
||||
|
||||
@@ -4,15 +4,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Final
|
||||
|
||||
from aioairzone_cloud.const import (
|
||||
AZD_ACTIVE,
|
||||
AZD_AIDOOS,
|
||||
AZD_ERRORS,
|
||||
AZD_PROBLEMS,
|
||||
AZD_SYSTEMS,
|
||||
AZD_WARNINGS,
|
||||
AZD_ZONES,
|
||||
)
|
||||
from aioairzone_cloud.const import AZD_PROBLEMS, AZD_WARNINGS, AZD_ZONES
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@@ -26,12 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
from .entity import (
|
||||
AirzoneAidooEntity,
|
||||
AirzoneEntity,
|
||||
AirzoneSystemEntity,
|
||||
AirzoneZoneEntity,
|
||||
)
|
||||
from .entity import AirzoneEntity, AirzoneZoneEntity
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -41,41 +28,7 @@ class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
attributes: dict[str, str] | None = None
|
||||
|
||||
|
||||
AIDOO_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = (
|
||||
AirzoneBinarySensorEntityDescription(
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
key=AZD_ACTIVE,
|
||||
),
|
||||
AirzoneBinarySensorEntityDescription(
|
||||
attributes={
|
||||
"errors": AZD_ERRORS,
|
||||
"warnings": AZD_WARNINGS,
|
||||
},
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
key=AZD_PROBLEMS,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
SYSTEM_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = (
|
||||
AirzoneBinarySensorEntityDescription(
|
||||
attributes={
|
||||
"errors": AZD_ERRORS,
|
||||
"warnings": AZD_WARNINGS,
|
||||
},
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
key=AZD_PROBLEMS,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = (
|
||||
AirzoneBinarySensorEntityDescription(
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
key=AZD_ACTIVE,
|
||||
),
|
||||
AirzoneBinarySensorEntityDescription(
|
||||
attributes={
|
||||
"warnings": AZD_WARNINGS,
|
||||
@@ -95,30 +48,6 @@ async def async_setup_entry(
|
||||
|
||||
binary_sensors: list[AirzoneBinarySensor] = []
|
||||
|
||||
for aidoo_id, aidoo_data in coordinator.data.get(AZD_AIDOOS, {}).items():
|
||||
for description in AIDOO_BINARY_SENSOR_TYPES:
|
||||
if description.key in aidoo_data:
|
||||
binary_sensors.append(
|
||||
AirzoneAidooBinarySensor(
|
||||
coordinator,
|
||||
description,
|
||||
aidoo_id,
|
||||
aidoo_data,
|
||||
)
|
||||
)
|
||||
|
||||
for system_id, system_data in coordinator.data.get(AZD_SYSTEMS, {}).items():
|
||||
for description in SYSTEM_BINARY_SENSOR_TYPES:
|
||||
if description.key in system_data:
|
||||
binary_sensors.append(
|
||||
AirzoneSystemBinarySensor(
|
||||
coordinator,
|
||||
description,
|
||||
system_id,
|
||||
system_data,
|
||||
)
|
||||
)
|
||||
|
||||
for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items():
|
||||
for description in ZONE_BINARY_SENSOR_TYPES:
|
||||
if description.key in zone_data:
|
||||
@@ -126,6 +55,7 @@ async def async_setup_entry(
|
||||
AirzoneZoneBinarySensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
zone_id,
|
||||
zone_data,
|
||||
)
|
||||
@@ -156,48 +86,6 @@ class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):
|
||||
}
|
||||
|
||||
|
||||
class AirzoneAidooBinarySensor(AirzoneAidooEntity, AirzoneBinarySensor):
|
||||
"""Define an Airzone Cloud Aidoo binary sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: AirzoneBinarySensorEntityDescription,
|
||||
aidoo_id: str,
|
||||
aidoo_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, aidoo_id, aidoo_data)
|
||||
|
||||
self._attr_unique_id = f"{aidoo_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
|
||||
class AirzoneSystemBinarySensor(AirzoneSystemEntity, AirzoneBinarySensor):
|
||||
"""Define an Airzone Cloud System binary sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: AirzoneBinarySensorEntityDescription,
|
||||
system_id: str,
|
||||
system_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, system_id, system_data)
|
||||
|
||||
self._attr_unique_id = f"{system_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
|
||||
class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor):
|
||||
"""Define an Airzone Cloud Zone binary sensor."""
|
||||
|
||||
@@ -207,11 +95,12 @@ class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor):
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: AirzoneBinarySensorEntityDescription,
|
||||
entry: ConfigEntry,
|
||||
zone_id: str,
|
||||
zone_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, zone_id, zone_data)
|
||||
super().__init__(coordinator, entry, zone_id, zone_data)
|
||||
|
||||
self._attr_unique_id = f"{zone_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""The Airzone Cloud integration coordinator."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioairzone_cloud.cloudapi import AirzoneCloudApi
|
||||
from aioairzone_cloud.exceptions import AirzoneCloudError
|
||||
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_CLOUD_TIMEOUT_SEC):
|
||||
async with async_timeout.timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC):
|
||||
try:
|
||||
await self.airzone.update()
|
||||
except AirzoneCloudError as error:
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import Any
|
||||
from aioairzone_cloud.const import (
|
||||
API_CITY,
|
||||
API_GROUP_ID,
|
||||
API_GROUPS,
|
||||
API_LOCATION_ID,
|
||||
API_OLD_ID,
|
||||
API_PIN,
|
||||
@@ -30,6 +29,7 @@ from .coordinator import AirzoneUpdateCoordinator
|
||||
|
||||
TO_REDACT_API = [
|
||||
API_CITY,
|
||||
API_GROUP_ID,
|
||||
API_LOCATION_ID,
|
||||
API_OLD_ID,
|
||||
API_PIN,
|
||||
@@ -58,17 +58,11 @@ def gather_ids(api_data: dict[str, Any]) -> dict[str, Any]:
|
||||
ids[dev_id] = f"device{dev_idx}"
|
||||
dev_idx += 1
|
||||
|
||||
group_idx = 1
|
||||
inst_idx = 1
|
||||
for inst_id, inst_data in api_data[RAW_INSTALLATIONS].items():
|
||||
for inst_id in api_data[RAW_INSTALLATIONS]:
|
||||
if inst_id not in ids:
|
||||
ids[inst_id] = f"installation{inst_idx}"
|
||||
inst_idx += 1
|
||||
for group in inst_data[API_GROUPS]:
|
||||
group_id = group[API_GROUP_ID]
|
||||
if group_id not in ids:
|
||||
ids[group_id] = f"group{group_idx}"
|
||||
group_idx += 1
|
||||
|
||||
ws_idx = 1
|
||||
for ws_id in api_data[RAW_WEBSERVERS]:
|
||||
|
||||
@@ -10,14 +10,14 @@ from aioairzone_cloud.const import (
|
||||
AZD_FIRMWARE,
|
||||
AZD_NAME,
|
||||
AZD_SYSTEM_ID,
|
||||
AZD_SYSTEMS,
|
||||
AZD_WEBSERVER,
|
||||
AZD_WEBSERVERS,
|
||||
AZD_ZONES,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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
|
||||
@@ -43,6 +43,7 @@ class AirzoneAidooEntity(AirzoneEntity):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
aidoo_id: str,
|
||||
aidoo_data: dict[str, Any],
|
||||
) -> None:
|
||||
@@ -66,41 +67,13 @@ class AirzoneAidooEntity(AirzoneEntity):
|
||||
return value
|
||||
|
||||
|
||||
class AirzoneSystemEntity(AirzoneEntity):
|
||||
"""Define an Airzone Cloud System entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
system_id: str,
|
||||
system_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.system_id = system_id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, system_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=system_data[AZD_NAME],
|
||||
via_device=(DOMAIN, system_data[AZD_WEBSERVER]),
|
||||
)
|
||||
|
||||
def get_airzone_value(self, key: str) -> Any:
|
||||
"""Return system value by key."""
|
||||
value = None
|
||||
if system := self.coordinator.data[AZD_SYSTEMS].get(self.system_id):
|
||||
value = system.get(key)
|
||||
return value
|
||||
|
||||
|
||||
class AirzoneWebServerEntity(AirzoneEntity):
|
||||
"""Define an Airzone Cloud WebServer entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
ws_id: str,
|
||||
ws_data: dict[str, Any],
|
||||
) -> None:
|
||||
@@ -131,6 +104,7 @@ class AirzoneZoneEntity(AirzoneEntity):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
zone_id: str,
|
||||
zone_data: dict[str, Any],
|
||||
) -> None:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.2.1"]
|
||||
"requirements": ["aioairzone-cloud==0.1.9"]
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ async def async_setup_entry(
|
||||
AirzoneAidooSensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
aidoo_id,
|
||||
aidoo_data,
|
||||
)
|
||||
@@ -102,6 +103,7 @@ async def async_setup_entry(
|
||||
AirzoneWebServerSensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
ws_id,
|
||||
ws_data,
|
||||
)
|
||||
@@ -115,6 +117,7 @@ async def async_setup_entry(
|
||||
AirzoneZoneSensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
zone_id,
|
||||
zone_data,
|
||||
)
|
||||
@@ -141,18 +144,18 @@ class AirzoneSensor(AirzoneEntity, SensorEntity):
|
||||
class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor):
|
||||
"""Define an Airzone Cloud Aidoo sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
entry: ConfigEntry,
|
||||
aidoo_id: str,
|
||||
aidoo_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, aidoo_id, aidoo_data)
|
||||
super().__init__(coordinator, entry, aidoo_id, aidoo_data)
|
||||
|
||||
self._attr_has_entity_name = True
|
||||
self._attr_unique_id = f"{aidoo_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
@@ -162,18 +165,18 @@ class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor):
|
||||
class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor):
|
||||
"""Define an Airzone Cloud WebServer sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
entry: ConfigEntry,
|
||||
ws_id: str,
|
||||
ws_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, ws_id, ws_data)
|
||||
super().__init__(coordinator, entry, ws_id, ws_data)
|
||||
|
||||
self._attr_has_entity_name = True
|
||||
self._attr_unique_id = f"{ws_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
@@ -183,18 +186,18 @@ class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor):
|
||||
class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor):
|
||||
"""Define an Airzone Cloud Zone sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
entry: ConfigEntry,
|
||||
zone_id: str,
|
||||
zone_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, zone_id, zone_data)
|
||||
super().__init__(coordinator, entry, zone_id, zone_data)
|
||||
|
||||
self._attr_has_entity_name = True
|
||||
self._attr_unique_id = f"{zone_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user