Merge pull request #53930 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2021-08-04 11:21:42 +02:00 committed by GitHub
commit f0f4c13cbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2439 changed files with 67515 additions and 24794 deletions

View File

@ -20,6 +20,8 @@ omit =
homeassistant/components/acmeda/helpers.py homeassistant/components/acmeda/helpers.py
homeassistant/components/acmeda/hub.py homeassistant/components/acmeda/hub.py
homeassistant/components/acmeda/sensor.py homeassistant/components/acmeda/sensor.py
homeassistant/components/adax/__init__.py
homeassistant/components/adax/climate.py
homeassistant/components/adguard/__init__.py homeassistant/components/adguard/__init__.py
homeassistant/components/adguard/const.py homeassistant/components/adguard/const.py
homeassistant/components/adguard/sensor.py homeassistant/components/adguard/sensor.py
@ -35,7 +37,6 @@ omit =
homeassistant/components/airnow/__init__.py homeassistant/components/airnow/__init__.py
homeassistant/components/airnow/sensor.py homeassistant/components/airnow/sensor.py
homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/__init__.py
homeassistant/components/airvisual/air_quality.py
homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual/sensor.py
homeassistant/components/aladdin_connect/* homeassistant/components/aladdin_connect/*
homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/__init__.py
@ -105,6 +106,8 @@ omit =
homeassistant/components/bloomsky/* homeassistant/components/bloomsky/*
homeassistant/components/bluesound/* homeassistant/components/bluesound/*
homeassistant/components/bluetooth_tracker/* homeassistant/components/bluetooth_tracker/*
homeassistant/components/bme280/__init__.py
homeassistant/components/bme280/const.py
homeassistant/components/bme280/sensor.py homeassistant/components/bme280/sensor.py
homeassistant/components/bme680/sensor.py homeassistant/components/bme680/sensor.py
homeassistant/components/bmp280/sensor.py homeassistant/components/bmp280/sensor.py
@ -131,9 +134,7 @@ omit =
homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/brottsplatskartan/sensor.py
homeassistant/components/browser/* homeassistant/components/browser/*
homeassistant/components/brunt/cover.py homeassistant/components/brunt/cover.py
homeassistant/components/bsblan/__init__.py
homeassistant/components/bsblan/climate.py homeassistant/components/bsblan/climate.py
homeassistant/components/bsblan/const.py
homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_home_hub_5/device_tracker.py
homeassistant/components/bt_smarthub/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py
homeassistant/components/buienradar/sensor.py homeassistant/components/buienradar/sensor.py
@ -153,7 +154,6 @@ omit =
homeassistant/components/clicksend/notify.py homeassistant/components/clicksend/notify.py
homeassistant/components/clicksend_tts/notify.py homeassistant/components/clicksend_tts/notify.py
homeassistant/components/cmus/media_player.py homeassistant/components/cmus/media_player.py
homeassistant/components/co2signal/*
homeassistant/components/coinbase/sensor.py homeassistant/components/coinbase/sensor.py
homeassistant/components/comed_hourly_pricing/sensor.py homeassistant/components/comed_hourly_pricing/sensor.py
homeassistant/components/comfoconnect/fan.py homeassistant/components/comfoconnect/fan.py
@ -275,6 +275,7 @@ omit =
homeassistant/components/esphome/fan.py homeassistant/components/esphome/fan.py
homeassistant/components/esphome/light.py homeassistant/components/esphome/light.py
homeassistant/components/esphome/number.py homeassistant/components/esphome/number.py
homeassistant/components/esphome/select.py
homeassistant/components/esphome/sensor.py homeassistant/components/esphome/sensor.py
homeassistant/components/esphome/switch.py homeassistant/components/esphome/switch.py
homeassistant/components/essent/sensor.py homeassistant/components/essent/sensor.py
@ -320,7 +321,8 @@ omit =
homeassistant/components/flick_electric/const.py homeassistant/components/flick_electric/const.py
homeassistant/components/flick_electric/sensor.py homeassistant/components/flick_electric/sensor.py
homeassistant/components/flock/notify.py homeassistant/components/flock/notify.py
homeassistant/components/flume/* homeassistant/components/flume/__init__.py
homeassistant/components/flume/sensor.py
homeassistant/components/flunearyou/__init__.py homeassistant/components/flunearyou/__init__.py
homeassistant/components/flunearyou/sensor.py homeassistant/components/flunearyou/sensor.py
homeassistant/components/flux_led/light.py homeassistant/components/flux_led/light.py
@ -348,7 +350,6 @@ omit =
homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/const.py
homeassistant/components/fritzbox_callmonitor/base.py homeassistant/components/fritzbox_callmonitor/base.py
homeassistant/components/fritzbox_callmonitor/sensor.py homeassistant/components/fritzbox_callmonitor/sensor.py
homeassistant/components/fritzbox_netmonitor/sensor.py
homeassistant/components/fronius/sensor.py homeassistant/components/fronius/sensor.py
homeassistant/components/frontier_silicon/media_player.py homeassistant/components/frontier_silicon/media_player.py
homeassistant/components/futurenow/light.py homeassistant/components/futurenow/light.py
@ -356,12 +357,9 @@ omit =
homeassistant/components/garages_amsterdam/__init__.py homeassistant/components/garages_amsterdam/__init__.py
homeassistant/components/garages_amsterdam/binary_sensor.py homeassistant/components/garages_amsterdam/binary_sensor.py
homeassistant/components/garages_amsterdam/sensor.py homeassistant/components/garages_amsterdam/sensor.py
homeassistant/components/garmin_connect/__init__.py
homeassistant/components/garmin_connect/const.py
homeassistant/components/garmin_connect/sensor.py
homeassistant/components/garmin_connect/alarm_util.py
homeassistant/components/gc100/* homeassistant/components/gc100/*
homeassistant/components/geniushub/* homeassistant/components/geniushub/*
homeassistant/components/generic_hygrostat/*
homeassistant/components/github/sensor.py homeassistant/components/github/sensor.py
homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitlab_ci/sensor.py
homeassistant/components/gitter/sensor.py homeassistant/components/gitter/sensor.py
@ -372,6 +370,7 @@ omit =
homeassistant/components/goalfeed/* homeassistant/components/goalfeed/*
homeassistant/components/goalzero/__init__.py homeassistant/components/goalzero/__init__.py
homeassistant/components/goalzero/binary_sensor.py homeassistant/components/goalzero/binary_sensor.py
homeassistant/components/goalzero/sensor.py
homeassistant/components/goalzero/switch.py homeassistant/components/goalzero/switch.py
homeassistant/components/google/* homeassistant/components/google/*
homeassistant/components/google_cloud/tts.py homeassistant/components/google_cloud/tts.py
@ -398,10 +397,6 @@ omit =
homeassistant/components/habitica/const.py homeassistant/components/habitica/const.py
homeassistant/components/habitica/sensor.py homeassistant/components/habitica/sensor.py
homeassistant/components/hangouts/* homeassistant/components/hangouts/*
homeassistant/components/hangouts/__init__.py
homeassistant/components/hangouts/const.py
homeassistant/components/hangouts/hangouts_bot.py
homeassistant/components/hangouts/hangups_utils.py
homeassistant/components/harman_kardon_avr/media_player.py homeassistant/components/harman_kardon_avr/media_player.py
homeassistant/components/harmony/const.py homeassistant/components/harmony/const.py
homeassistant/components/harmony/data.py homeassistant/components/harmony/data.py
@ -415,7 +410,8 @@ omit =
homeassistant/components/heatmiser/climate.py homeassistant/components/heatmiser/climate.py
homeassistant/components/hikvision/binary_sensor.py homeassistant/components/hikvision/binary_sensor.py
homeassistant/components/hikvisioncam/switch.py homeassistant/components/hikvisioncam/switch.py
homeassistant/components/hisense_aehw4a1/* homeassistant/components/hisense_aehw4a1/__init__.py
homeassistant/components/hisense_aehw4a1/climate.py
homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hitron_coda/device_tracker.py
homeassistant/components/hive/__init__.py homeassistant/components/hive/__init__.py
homeassistant/components/hive/climate.py homeassistant/components/hive/climate.py
@ -426,15 +422,18 @@ omit =
homeassistant/components/hive/water_heater.py homeassistant/components/hive/water_heater.py
homeassistant/components/hlk_sw16/__init__.py homeassistant/components/hlk_sw16/__init__.py
homeassistant/components/hlk_sw16/switch.py homeassistant/components/hlk_sw16/switch.py
homeassistant/components/home_connect/* homeassistant/components/home_connect/__init__.py
homeassistant/components/home_connect/api.py
homeassistant/components/home_connect/binary_sensor.py
homeassistant/components/home_connect/entity.py
homeassistant/components/home_connect/light.py
homeassistant/components/home_connect/sensor.py
homeassistant/components/home_connect/switch.py
homeassistant/components/homematic/* homeassistant/components/homematic/*
homeassistant/components/homematic/climate.py
homeassistant/components/homematic/cover.py
homeassistant/components/homematic/notify.py
homeassistant/components/home_plus_control/api.py homeassistant/components/home_plus_control/api.py
homeassistant/components/home_plus_control/helpers.py
homeassistant/components/home_plus_control/switch.py homeassistant/components/home_plus_control/switch.py
homeassistant/components/homeworks/* homeassistant/components/homeworks/*
homeassistant/components/honeywell/__init__.py
homeassistant/components/honeywell/climate.py homeassistant/components/honeywell/climate.py
homeassistant/components/horizon/media_player.py homeassistant/components/horizon/media_player.py
homeassistant/components/hp_ilo/sensor.py homeassistant/components/hp_ilo/sensor.py
@ -525,8 +524,6 @@ omit =
homeassistant/components/kira/* homeassistant/components/kira/*
homeassistant/components/kiwi/lock.py homeassistant/components/kiwi/lock.py
homeassistant/components/knx/* homeassistant/components/knx/*
homeassistant/components/knx/climate.py
homeassistant/components/knx/cover.py
homeassistant/components/kodi/__init__.py homeassistant/components/kodi/__init__.py
homeassistant/components/kodi/browse_media.py homeassistant/components/kodi/browse_media.py
homeassistant/components/kodi/const.py homeassistant/components/kodi/const.py
@ -624,6 +621,7 @@ omit =
homeassistant/components/mill/__init__.py homeassistant/components/mill/__init__.py
homeassistant/components/mill/climate.py homeassistant/components/mill/climate.py
homeassistant/components/mill/const.py homeassistant/components/mill/const.py
homeassistant/components/mill/sensor.py
homeassistant/components/minecraft_server/__init__.py homeassistant/components/minecraft_server/__init__.py
homeassistant/components/minecraft_server/binary_sensor.py homeassistant/components/minecraft_server/binary_sensor.py
homeassistant/components/minecraft_server/const.py homeassistant/components/minecraft_server/const.py
@ -638,6 +636,7 @@ omit =
homeassistant/components/modbus/cover.py homeassistant/components/modbus/cover.py
homeassistant/components/modbus/climate.py homeassistant/components/modbus/climate.py
homeassistant/components/modbus/modbus.py homeassistant/components/modbus/modbus.py
homeassistant/components/modbus/validators.py
homeassistant/components/modem_callerid/sensor.py homeassistant/components/modem_callerid/sensor.py
homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/__init__.py
homeassistant/components/motion_blinds/const.py homeassistant/components/motion_blinds/const.py
@ -655,7 +654,6 @@ omit =
homeassistant/components/mvglive/sensor.py homeassistant/components/mvglive/sensor.py
homeassistant/components/mychevy/* homeassistant/components/mychevy/*
homeassistant/components/mycroft/* homeassistant/components/mycroft/*
homeassistant/components/mycroft/notify.py
homeassistant/components/mysensors/__init__.py homeassistant/components/mysensors/__init__.py
homeassistant/components/mysensors/binary_sensor.py homeassistant/components/mysensors/binary_sensor.py
homeassistant/components/mysensors/climate.py homeassistant/components/mysensors/climate.py
@ -692,6 +690,7 @@ omit =
homeassistant/components/neurio_energy/sensor.py homeassistant/components/neurio_energy/sensor.py
homeassistant/components/nexia/climate.py homeassistant/components/nexia/climate.py
homeassistant/components/nextcloud/* homeassistant/components/nextcloud/*
homeassistant/components/nfandroidtv/__init__.py
homeassistant/components/nfandroidtv/notify.py homeassistant/components/nfandroidtv/notify.py
homeassistant/components/niko_home_control/light.py homeassistant/components/niko_home_control/light.py
homeassistant/components/nilu/air_quality.py homeassistant/components/nilu/air_quality.py
@ -827,8 +826,6 @@ omit =
homeassistant/components/radarr/sensor.py homeassistant/components/radarr/sensor.py
homeassistant/components/radiotherm/climate.py homeassistant/components/radiotherm/climate.py
homeassistant/components/rainbird/* homeassistant/components/rainbird/*
homeassistant/components/rainbird/sensor.py
homeassistant/components/rainbird/switch.py
homeassistant/components/raincloud/* homeassistant/components/raincloud/*
homeassistant/components/rainmachine/__init__.py homeassistant/components/rainmachine/__init__.py
homeassistant/components/rainmachine/binary_sensor.py homeassistant/components/rainmachine/binary_sensor.py
@ -875,7 +872,6 @@ omit =
homeassistant/components/rova/sensor.py homeassistant/components/rova/sensor.py
homeassistant/components/rpi_camera/* homeassistant/components/rpi_camera/*
homeassistant/components/rpi_gpio/* homeassistant/components/rpi_gpio/*
homeassistant/components/rpi_gpio/cover.py
homeassistant/components/rpi_gpio_pwm/light.py homeassistant/components/rpi_gpio_pwm/light.py
homeassistant/components/rpi_pfio/* homeassistant/components/rpi_pfio/*
homeassistant/components/rpi_rf/switch.py homeassistant/components/rpi_rf/switch.py
@ -895,7 +891,6 @@ omit =
homeassistant/components/screenlogic/services.py homeassistant/components/screenlogic/services.py
homeassistant/components/screenlogic/switch.py homeassistant/components/screenlogic/switch.py
homeassistant/components/scsgate/* homeassistant/components/scsgate/*
homeassistant/components/scsgate/cover.py
homeassistant/components/sendgrid/notify.py homeassistant/components/sendgrid/notify.py
homeassistant/components/sense/* homeassistant/components/sense/*
homeassistant/components/sensehat/light.py homeassistant/components/sensehat/light.py
@ -973,7 +968,6 @@ omit =
homeassistant/components/sonos/* homeassistant/components/sonos/*
homeassistant/components/sony_projector/switch.py homeassistant/components/sony_projector/switch.py
homeassistant/components/spc/* homeassistant/components/spc/*
homeassistant/components/speedtestdotnet/*
homeassistant/components/spider/* homeassistant/components/spider/*
homeassistant/components/splunk/* homeassistant/components/splunk/*
homeassistant/components/spotify/__init__.py homeassistant/components/spotify/__init__.py
@ -998,8 +992,6 @@ omit =
homeassistant/components/swiss_public_transport/sensor.py homeassistant/components/swiss_public_transport/sensor.py
homeassistant/components/swisscom/device_tracker.py homeassistant/components/swisscom/device_tracker.py
homeassistant/components/switchbot/switch.py homeassistant/components/switchbot/switch.py
homeassistant/components/switcher_kis/sensor.py
homeassistant/components/switcher_kis/switch.py
homeassistant/components/switchmate/switch.py homeassistant/components/switchmate/switch.py
homeassistant/components/syncthing/__init__.py homeassistant/components/syncthing/__init__.py
homeassistant/components/syncthing/sensor.py homeassistant/components/syncthing/sensor.py
@ -1020,7 +1012,6 @@ omit =
homeassistant/components/system_bridge/sensor.py homeassistant/components/system_bridge/sensor.py
homeassistant/components/systemmonitor/sensor.py homeassistant/components/systemmonitor/sensor.py
homeassistant/components/tado/* homeassistant/components/tado/*
homeassistant/components/tado/device_tracker.py
homeassistant/components/tahoma/* homeassistant/components/tahoma/*
homeassistant/components/tank_utility/sensor.py homeassistant/components/tank_utility/sensor.py
homeassistant/components/tankerkoenig/* homeassistant/components/tankerkoenig/*
@ -1088,9 +1079,6 @@ omit =
homeassistant/components/traccar/const.py homeassistant/components/traccar/const.py
homeassistant/components/trackr/device_tracker.py homeassistant/components/trackr/device_tracker.py
homeassistant/components/tradfri/* homeassistant/components/tradfri/*
homeassistant/components/tradfri/light.py
homeassistant/components/tradfri/cover.py
homeassistant/components/tradfri/base_class.py
homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_train/sensor.py
homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py
homeassistant/components/transmission/sensor.py homeassistant/components/transmission/sensor.py
@ -1211,20 +1199,29 @@ omit =
homeassistant/components/xiaomi_miio/device_tracker.py homeassistant/components/xiaomi_miio/device_tracker.py
homeassistant/components/xiaomi_miio/fan.py homeassistant/components/xiaomi_miio/fan.py
homeassistant/components/xiaomi_miio/gateway.py homeassistant/components/xiaomi_miio/gateway.py
homeassistant/components/xiaomi_miio/humidifier.py
homeassistant/components/xiaomi_miio/light.py homeassistant/components/xiaomi_miio/light.py
homeassistant/components/xiaomi_miio/number.py
homeassistant/components/xiaomi_miio/remote.py homeassistant/components/xiaomi_miio/remote.py
homeassistant/components/xiaomi_miio/select.py
homeassistant/components/xiaomi_miio/sensor.py homeassistant/components/xiaomi_miio/sensor.py
homeassistant/components/xiaomi_miio/switch.py homeassistant/components/xiaomi_miio/switch.py
homeassistant/components/xiaomi_miio/vacuum.py homeassistant/components/xiaomi_miio/vacuum.py
homeassistant/components/xiaomi_tv/media_player.py homeassistant/components/xiaomi_tv/media_player.py
homeassistant/components/xmpp/notify.py homeassistant/components/xmpp/notify.py
homeassistant/components/xs1/* homeassistant/components/xs1/*
homeassistant/components/yale_smart_alarm/__init__.py
homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yale_smart_alarm/alarm_control_panel.py
homeassistant/components/yale_smart_alarm/const.py
homeassistant/components/yale_smart_alarm/coordinator.py
homeassistant/components/yamaha_musiccast/__init__.py homeassistant/components/yamaha_musiccast/__init__.py
homeassistant/components/yamaha_musiccast/media_player.py homeassistant/components/yamaha_musiccast/media_player.py
homeassistant/components/yandex_transport/* homeassistant/components/yandex_transport/*
homeassistant/components/yeelightsunflower/light.py homeassistant/components/yeelightsunflower/light.py
homeassistant/components/yi/camera.py homeassistant/components/yi/camera.py
homeassistant/components/youless/__init__.py
homeassistant/components/youless/const.py
homeassistant/components/youless/sensor.py
homeassistant/components/zabbix/* homeassistant/components/zabbix/*
homeassistant/components/zamg/sensor.py homeassistant/components/zamg/sensor.py
homeassistant/components/zamg/weather.py homeassistant/components/zamg/weather.py

View File

@ -115,7 +115,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2021.06.2 uses: home-assistant/builder@2021.07.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@ -134,6 +134,7 @@ jobs:
machine: machine:
- generic-x86-64 - generic-x86-64
- intel-nuc - intel-nuc
- khadas-vim3
- odroid-c2 - odroid-c2
- odroid-c4 - odroid-c4
- odroid-n2 - odroid-n2
@ -167,7 +168,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: home-assistant/builder@2021.06.2 uses: home-assistant/builder@2021.07.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \

View File

@ -740,4 +740,4 @@ jobs:
coverage report --fail-under=94 coverage report --fail-under=94
coverage xml coverage xml
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v1.5.2 uses: codecov/codecov-action@v2.0.2

View File

@ -9,7 +9,7 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v2.0.3 - uses: dessant/lock-threads@v2.1.1
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-lock-inactive-days: "30" issue-lock-inactive-days: "30"

View File

@ -16,7 +16,7 @@ jobs:
# - No PRs marked as no-stale # - No PRs marked as no-stale
# - No issues marked as no-stale or help-wanted # - No issues marked as no-stale or help-wanted
- name: 90 days stale issues & PRs policy - name: 90 days stale issues & PRs policy
uses: actions/stale@v3.0.19 uses: actions/stale@v4
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90 days-before-stale: 90
@ -53,7 +53,7 @@ jobs:
# - No PRs marked as no-stale or new-integrations # - No PRs marked as no-stale or new-integrations
# - No issues (-1) # - No issues (-1)
- name: 30 days stale PRs policy - name: 30 days stale PRs policy
uses: actions/stale@v3.0.19 uses: actions/stale@v4
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30 days-before-stale: 30
@ -78,7 +78,7 @@ jobs:
# - No Issues marked as no-stale or help-wanted # - No Issues marked as no-stale or help-wanted
# - No PRs (-1) # - No PRs (-1)
- name: Needs more information stale issues policy - name: Needs more information stale issues policy
uses: actions/stale@v3.0.19 uses: actions/stale@v4
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "needs-more-information" only-labels: "needs-more-information"

View File

@ -81,7 +81,7 @@ jobs:
name: requirements_diff name: requirements_diff
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2021.06.0 uses: home-assistant/wheels@2021.07.0
with: with:
tag: ${{ matrix.tag }} tag: ${{ matrix.tag }}
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
@ -150,7 +150,7 @@ jobs:
done done
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2021.06.0 uses: home-assistant/wheels@2021.07.0
with: with:
tag: ${{ matrix.tag }} tag: ${{ matrix.tag }}
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}

View File

@ -1,11 +1,11 @@
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.16.0 rev: v2.23.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 21.6b0 rev: 21.7b0
hooks: hooks:
- id: black - id: black
args: args:
@ -70,7 +70,7 @@ repos:
- id: prettier - id: prettier
stages: [manual] stages: [manual]
- repo: https://github.com/cdce8p/python-typing-update - repo: https://github.com/cdce8p/python-typing-update
rev: v0.3.3 rev: v0.3.5
hooks: hooks:
# Run `python-typing-update` hook manually from time to time # Run `python-typing-update` hook manually from time to time
# to update python typing syntax. # to update python typing syntax.

View File

@ -9,15 +9,18 @@ homeassistant.components.actiontec.*
homeassistant.components.aftership.* homeassistant.components.aftership.*
homeassistant.components.air_quality.* homeassistant.components.air_quality.*
homeassistant.components.airly.* homeassistant.components.airly.*
homeassistant.components.airvisual.*
homeassistant.components.aladdin_connect.* homeassistant.components.aladdin_connect.*
homeassistant.components.alarm_control_panel.* homeassistant.components.alarm_control_panel.*
homeassistant.components.amazon_polly.* homeassistant.components.amazon_polly.*
homeassistant.components.ambee.* homeassistant.components.ambee.*
homeassistant.components.ambient_station.*
homeassistant.components.ampio.* homeassistant.components.ampio.*
homeassistant.components.automation.* homeassistant.components.automation.*
homeassistant.components.binary_sensor.* homeassistant.components.binary_sensor.*
homeassistant.components.bluetooth_tracker.* homeassistant.components.bluetooth_tracker.*
homeassistant.components.bond.* homeassistant.components.bond.*
homeassistant.components.braviatv.*
homeassistant.components.brother.* homeassistant.components.brother.*
homeassistant.components.calendar.* homeassistant.components.calendar.*
homeassistant.components.camera.* homeassistant.components.camera.*
@ -25,17 +28,24 @@ homeassistant.components.canary.*
homeassistant.components.cover.* homeassistant.components.cover.*
homeassistant.components.device_automation.* homeassistant.components.device_automation.*
homeassistant.components.device_tracker.* homeassistant.components.device_tracker.*
homeassistant.components.devolo_home_control.*
homeassistant.components.dnsip.* homeassistant.components.dnsip.*
homeassistant.components.dsmr.* homeassistant.components.dsmr.*
homeassistant.components.dunehd.* homeassistant.components.dunehd.*
homeassistant.components.elgato.* homeassistant.components.elgato.*
homeassistant.components.esphome.*
homeassistant.components.energy.*
homeassistant.components.fastdotcom.*
homeassistant.components.fitbit.* homeassistant.components.fitbit.*
homeassistant.components.flunearyou.*
homeassistant.components.forecast_solar.* homeassistant.components.forecast_solar.*
homeassistant.components.fritzbox.* homeassistant.components.fritzbox.*
homeassistant.components.frontend.* homeassistant.components.frontend.*
homeassistant.components.fritz.*
homeassistant.components.geo_location.* homeassistant.components.geo_location.*
homeassistant.components.gios.* homeassistant.components.gios.*
homeassistant.components.group.* homeassistant.components.group.*
homeassistant.components.guardian.*
homeassistant.components.history.* homeassistant.components.history.*
homeassistant.components.homeassistant.triggers.event homeassistant.components.homeassistant.triggers.event
homeassistant.components.http.* homeassistant.components.http.*
@ -45,6 +55,7 @@ homeassistant.components.image_processing.*
homeassistant.components.integration.* homeassistant.components.integration.*
homeassistant.components.knx.* homeassistant.components.knx.*
homeassistant.components.kraken.* homeassistant.components.kraken.*
homeassistant.components.lcn.*
homeassistant.components.light.* homeassistant.components.light.*
homeassistant.components.local_ip.* homeassistant.components.local_ip.*
homeassistant.components.lock.* homeassistant.components.lock.*
@ -52,30 +63,43 @@ homeassistant.components.mailbox.*
homeassistant.components.media_player.* homeassistant.components.media_player.*
homeassistant.components.mysensors.* homeassistant.components.mysensors.*
homeassistant.components.nam.* homeassistant.components.nam.*
homeassistant.components.nest.*
homeassistant.components.netatmo.*
homeassistant.components.network.* homeassistant.components.network.*
homeassistant.components.no_ip.* homeassistant.components.no_ip.*
homeassistant.components.notify.* homeassistant.components.notify.*
homeassistant.components.notion.*
homeassistant.components.number.* homeassistant.components.number.*
homeassistant.components.onewire.* homeassistant.components.onewire.*
homeassistant.components.openuv.*
homeassistant.components.persistent_notification.* homeassistant.components.persistent_notification.*
homeassistant.components.pi_hole.* homeassistant.components.pi_hole.*
homeassistant.components.proximity.* homeassistant.components.proximity.*
homeassistant.components.rainmachine.*
homeassistant.components.recollect_waste.*
homeassistant.components.recorder.purge homeassistant.components.recorder.purge
homeassistant.components.recorder.repack homeassistant.components.recorder.repack
homeassistant.components.recorder.statistics homeassistant.components.recorder.statistics
homeassistant.components.remote.* homeassistant.components.remote.*
homeassistant.components.renault.*
homeassistant.components.rituals_perfume_genie.*
homeassistant.components.scene.* homeassistant.components.scene.*
homeassistant.components.select.* homeassistant.components.select.*
homeassistant.components.sensor.* homeassistant.components.sensor.*
homeassistant.components.shelly.*
homeassistant.components.simplisafe.*
homeassistant.components.slack.* homeassistant.components.slack.*
homeassistant.components.sonos.media_player homeassistant.components.sonos.media_player
homeassistant.components.ssdp.* homeassistant.components.ssdp.*
homeassistant.components.stream.* homeassistant.components.stream.*
homeassistant.components.sun.* homeassistant.components.sun.*
homeassistant.components.switch.* homeassistant.components.switch.*
homeassistant.components.switcher_kis.*
homeassistant.components.synology_dsm.* homeassistant.components.synology_dsm.*
homeassistant.components.systemmonitor.* homeassistant.components.systemmonitor.*
homeassistant.components.tag.*
homeassistant.components.tcp.* homeassistant.components.tcp.*
homeassistant.components.tile.*
homeassistant.components.tts.* homeassistant.components.tts.*
homeassistant.components.upcloud.* homeassistant.components.upcloud.*
homeassistant.components.uptime.* homeassistant.components.uptime.*

15
.vscode/tasks.json vendored
View File

@ -2,13 +2,10 @@
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"label": "Preview", "label": "Run Home Assistant Core",
"type": "shell", "type": "shell",
"command": "hass -c ./config", "command": "hass -c ./config",
"group": { "group": "test",
"kind": "test",
"isDefault": true
},
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new" "panel": "new"
@ -19,7 +16,9 @@
"label": "Pytest", "label": "Pytest",
"type": "shell", "type": "shell",
"command": "pytest --timeout=10 tests", "command": "pytest --timeout=10 tests",
"dependsOn": ["Install all Test Requirements"], "dependsOn": [
"Install all Test Requirements"
],
"group": { "group": {
"kind": "test", "kind": "test",
"isDefault": true "isDefault": true
@ -48,7 +47,9 @@
"label": "Pylint", "label": "Pylint",
"type": "shell", "type": "shell",
"command": "pylint homeassistant", "command": "pylint homeassistant",
"dependsOn": ["Install all Requirements"], "dependsOn": [
"Install all Requirements"
],
"group": { "group": {
"kind": "test", "kind": "test",
"isDefault": true "isDefault": true

View File

@ -22,6 +22,7 @@ homeassistant/scripts/check_config.py @kellerza
homeassistant/components/abode/* @shred86 homeassistant/components/abode/* @shred86
homeassistant/components/accuweather/* @bieniu homeassistant/components/accuweather/* @bieniu
homeassistant/components/acmeda/* @atmurray homeassistant/components/acmeda/* @atmurray
homeassistant/components/adax/* @danielhiversen
homeassistant/components/adguard/* @frenck homeassistant/components/adguard/* @frenck
homeassistant/components/advantage_air/* @Bre77 homeassistant/components/advantage_air/* @Bre77
homeassistant/components/aemet/* @noltari homeassistant/components/aemet/* @noltari
@ -97,7 +98,7 @@ homeassistant/components/configurator/* @home-assistant/core
homeassistant/components/control4/* @lawtancool homeassistant/components/control4/* @lawtancool
homeassistant/components/conversation/* @home-assistant/core homeassistant/components/conversation/* @home-assistant/core
homeassistant/components/coolmaster/* @OnFreund homeassistant/components/coolmaster/* @OnFreund
homeassistant/components/coronavirus/* @home_assistant/core homeassistant/components/coronavirus/* @home-assistant/core
homeassistant/components/counter/* @fabaff homeassistant/components/counter/* @fabaff
homeassistant/components/cover/* @home-assistant/core homeassistant/components/cover/* @home-assistant/core
homeassistant/components/cpuspeed/* @fabaff homeassistant/components/cpuspeed/* @fabaff
@ -139,6 +140,7 @@ homeassistant/components/emby/* @mezz64
homeassistant/components/emoncms/* @borpin homeassistant/components/emoncms/* @borpin
homeassistant/components/emonitor/* @bdraco homeassistant/components/emonitor/* @bdraco
homeassistant/components/emulated_kasa/* @kbickar homeassistant/components/emulated_kasa/* @kbickar
homeassistant/components/energy/* @home-assistant/core
homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enigma2/* @fbradyirl
homeassistant/components/enocean/* @bdurrer homeassistant/components/enocean/* @bdurrer
homeassistant/components/enphase_envoy/* @gtdiehl homeassistant/components/enphase_envoy/* @gtdiehl
@ -160,6 +162,7 @@ homeassistant/components/fireservicerota/* @cyberjunky
homeassistant/components/firmata/* @DaAwesomeP homeassistant/components/firmata/* @DaAwesomeP
homeassistant/components/fixer/* @fabaff homeassistant/components/fixer/* @fabaff
homeassistant/components/flick_electric/* @ZephireNZ homeassistant/components/flick_electric/* @ZephireNZ
homeassistant/components/flipr/* @cnico
homeassistant/components/flo/* @dmulcahey homeassistant/components/flo/* @dmulcahey
homeassistant/components/flock/* @fabaff homeassistant/components/flock/* @fabaff
homeassistant/components/flume/* @ChrisMandich @bdraco homeassistant/components/flume/* @ChrisMandich @bdraco
@ -175,8 +178,8 @@ homeassistant/components/fritzbox/* @mib1185
homeassistant/components/fronius/* @nielstron homeassistant/components/fronius/* @nielstron
homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/frontend/* @home-assistant/frontend
homeassistant/components/garages_amsterdam/* @klaasnicolaas homeassistant/components/garages_amsterdam/* @klaasnicolaas
homeassistant/components/garmin_connect/* @cyberjunky
homeassistant/components/gdacs/* @exxamalte homeassistant/components/gdacs/* @exxamalte
homeassistant/components/generic_hygrostat/* @Shulyaka
homeassistant/components/geniushub/* @zxdavb homeassistant/components/geniushub/* @zxdavb
homeassistant/components/geo_json_events/* @exxamalte homeassistant/components/geo_json_events/* @exxamalte
homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geo_rss_events/* @exxamalte
@ -213,6 +216,7 @@ homeassistant/components/homeassistant/* @home-assistant/core
homeassistant/components/homekit/* @bdraco homeassistant/components/homekit/* @bdraco
homeassistant/components/homekit_controller/* @Jc2k @bdraco homeassistant/components/homekit_controller/* @Jc2k @bdraco
homeassistant/components/homematic/* @pvizeli @danielperna84 homeassistant/components/homematic/* @pvizeli @danielperna84
homeassistant/components/honeywell/* @rdfurman
homeassistant/components/http/* @home-assistant/core homeassistant/components/http/* @home-assistant/core
homeassistant/components/huawei_lte/* @scop @fphammerle homeassistant/components/huawei_lte/* @scop @fphammerle
homeassistant/components/huawei_router/* @abmantis homeassistant/components/huawei_router/* @abmantis
@ -329,6 +333,7 @@ homeassistant/components/netdata/* @fabaff
homeassistant/components/nexia/* @bdraco homeassistant/components/nexia/* @bdraco
homeassistant/components/nextbus/* @vividboarder homeassistant/components/nextbus/* @vividboarder
homeassistant/components/nextcloud/* @meichthys homeassistant/components/nextcloud/* @meichthys
homeassistant/components/nfandroidtv/* @tkdrob
homeassistant/components/nightscout/* @marciogranzotto homeassistant/components/nightscout/* @marciogranzotto
homeassistant/components/nilu/* @hfurubotten homeassistant/components/nilu/* @hfurubotten
homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nissan_leaf/* @filcole
@ -384,6 +389,7 @@ homeassistant/components/powerwall/* @bdraco @jrester
homeassistant/components/profiler/* @bdraco homeassistant/components/profiler/* @bdraco
homeassistant/components/progettihwsw/* @ardaseremet homeassistant/components/progettihwsw/* @ardaseremet
homeassistant/components/prometheus/* @knyar homeassistant/components/prometheus/* @knyar
homeassistant/components/prosegur/* @dgomes
homeassistant/components/proxmoxve/* @k4ds3 @jhollowe @Corbeno homeassistant/components/proxmoxve/* @k4ds3 @jhollowe @Corbeno
homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ps4/* @ktnrg45
homeassistant/components/push/* @dgomes homeassistant/components/push/* @dgomes
@ -404,6 +410,7 @@ homeassistant/components/rainmachine/* @bachya
homeassistant/components/random/* @fabaff homeassistant/components/random/* @fabaff
homeassistant/components/recollect_waste/* @bachya homeassistant/components/recollect_waste/* @bachya
homeassistant/components/rejseplanen/* @DarkFox homeassistant/components/rejseplanen/* @DarkFox
homeassistant/components/renault/* @epenet
homeassistant/components/repetier/* @MTrab homeassistant/components/repetier/* @MTrab
homeassistant/components/rflink/* @javicalle homeassistant/components/rflink/* @javicalle
homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221 homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221
@ -442,6 +449,7 @@ homeassistant/components/sighthound/* @robmarkcole
homeassistant/components/signal_messenger/* @bbernhard homeassistant/components/signal_messenger/* @bbernhard
homeassistant/components/simplisafe/* @bachya homeassistant/components/simplisafe/* @bachya
homeassistant/components/sinch/* @bendikrb homeassistant/components/sinch/* @bendikrb
homeassistant/components/siren/* @home-assistant/core @raman325
homeassistant/components/sisyphus/* @jkeljo homeassistant/components/sisyphus/* @jkeljo
homeassistant/components/sky_hub/* @rogerselwyn homeassistant/components/sky_hub/* @rogerselwyn
homeassistant/components/slack/* @bachya homeassistant/components/slack/* @bachya
@ -502,7 +510,7 @@ homeassistant/components/tapsaff/* @bazwilliams
homeassistant/components/tasmota/* @emontnemery homeassistant/components/tasmota/* @emontnemery
homeassistant/components/tautulli/* @ludeeus homeassistant/components/tautulli/* @ludeeus
homeassistant/components/tellduslive/* @fredrike homeassistant/components/tellduslive/* @fredrike
homeassistant/components/template/* @PhracturedBlue @tetienne homeassistant/components/template/* @PhracturedBlue @tetienne @home-assistant/core
homeassistant/components/tesla/* @zabuldon @alandtse homeassistant/components/tesla/* @zabuldon @alandtse
homeassistant/components/tfiac/* @fredrike @mellado homeassistant/components/tfiac/* @fredrike @mellado
homeassistant/components/thethingsnetwork/* @fabaff homeassistant/components/thethingsnetwork/* @fabaff
@ -554,11 +562,12 @@ homeassistant/components/wallbox/* @hesselonline
homeassistant/components/waqi/* @andrey-git homeassistant/components/waqi/* @andrey-git
homeassistant/components/watson_tts/* @rutkai homeassistant/components/watson_tts/* @rutkai
homeassistant/components/weather/* @fabaff homeassistant/components/weather/* @fabaff
homeassistant/components/webostv/* @bendavid homeassistant/components/webostv/* @bendavid @thecode
homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/websocket_api/* @home-assistant/core
homeassistant/components/wemo/* @esev homeassistant/components/wemo/* @esev
homeassistant/components/wiffi/* @mampfes homeassistant/components/wiffi/* @mampfes
homeassistant/components/wilight/* @leofig-rj homeassistant/components/wilight/* @leofig-rj
homeassistant/components/wirelesstag/* @sergeymaysak
homeassistant/components/withings/* @vangorra homeassistant/components/withings/* @vangorra
homeassistant/components/wled/* @frenck homeassistant/components/wled/* @frenck
homeassistant/components/wolflink/* @adamkrol93 homeassistant/components/wolflink/* @adamkrol93
@ -576,6 +585,7 @@ homeassistant/components/yandex_transport/* @rishatik92 @devbis
homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn
homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yeelightsunflower/* @lindsaymarkward
homeassistant/components/yi/* @bachya homeassistant/components/yi/* @bachya
homeassistant/components/youless/* @gjong
homeassistant/components/zeroconf/* @bdraco homeassistant/components/zeroconf/* @bdraco
homeassistant/components/zerproc/* @emlove homeassistant/components/zerproc/* @emlove
homeassistant/components/zha/* @dmulcahey @adminiuga homeassistant/components/zha/* @dmulcahey @adminiuga

View File

@ -2,11 +2,11 @@
"image": "homeassistant/{arch}-homeassistant", "image": "homeassistant/{arch}-homeassistant",
"shadow_repository": "ghcr.io/home-assistant", "shadow_repository": "ghcr.io/home-assistant",
"build_from": { "build_from": {
"aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.06.2", "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.07.0",
"armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.06.2", "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.07.0",
"armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.06.2", "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.07.0",
"amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.06.2", "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.07.0",
"i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.06.2" "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.07.0"
}, },
"labels": { "labels": {
"io.hass.type": "core", "io.hass.type": "core",

View File

@ -146,8 +146,8 @@ def daemonize() -> None:
# redirect standard file descriptors to devnull # redirect standard file descriptors to devnull
# pylint: disable=consider-using-with # pylint: disable=consider-using-with
infd = open(os.devnull) infd = open(os.devnull, encoding="utf8")
outfd = open(os.devnull, "a+") outfd = open(os.devnull, "a+", encoding="utf8")
sys.stdout.flush() sys.stdout.flush()
sys.stderr.flush() sys.stderr.flush()
os.dup2(infd.fileno(), sys.stdin.fileno()) os.dup2(infd.fileno(), sys.stdin.fileno())
@ -159,7 +159,7 @@ def check_pid(pid_file: str) -> None:
"""Check that Home Assistant is not already running.""" """Check that Home Assistant is not already running."""
# Check pid file # Check pid file
try: try:
with open(pid_file) as file: with open(pid_file, encoding="utf8") as file:
pid = int(file.readline()) pid = int(file.readline())
except OSError: except OSError:
# PID File does not exist # PID File does not exist
@ -182,7 +182,7 @@ def write_pid(pid_file: str) -> None:
"""Create a PID File.""" """Create a PID File."""
pid = os.getpid() pid = os.getpid()
try: try:
with open(pid_file, "w") as file: with open(pid_file, "w", encoding="utf8") as file:
file.write(str(pid)) file.write(str(pid))
except OSError: except OSError:
print(f"Fatal Error: Unable to write pid file {pid_file}") print(f"Fatal Error: Unable to write pid file {pid_file}")

View File

@ -1,7 +1,7 @@
"""Auth provider that validates credentials via an external command.""" """Auth provider that validates credentials via an external command."""
from __future__ import annotations from __future__ import annotations
import asyncio.subprocess import asyncio
import collections import collections
from collections.abc import Mapping from collections.abc import Mapping
import logging import logging
@ -64,7 +64,7 @@ class CommandLineAuthProvider(AuthProvider):
"""Validate a username and password.""" """Validate a username and password."""
env = {"username": username, "password": password} env = {"username": username, "password": password}
try: try:
process = await asyncio.subprocess.create_subprocess_exec( # pylint: disable=no-member process = await asyncio.create_subprocess_exec(
self.config[CONF_COMMAND], self.config[CONF_COMMAND],
*self.config[CONF_ARGS], *self.config[CONF_ARGS],
env=env, env=env,

View File

@ -1,5 +1,4 @@
"""Support for the Abode Security System.""" """Support for the Abode Security System."""
from copy import deepcopy
from functools import partial from functools import partial
from abodepy import Abode from abodepy import Abode
@ -8,7 +7,6 @@ import abodepy.helpers.timeline as TIMELINE
from requests.exceptions import ConnectTimeout, HTTPError from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
ATTR_DATE, ATTR_DATE,
@ -44,22 +42,7 @@ ATTR_APP_TYPE = "app_type"
ATTR_EVENT_BY = "event_by" ATTR_EVENT_BY = "event_by"
ATTR_VALUE = "value" ATTR_VALUE = "value"
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = cv.deprecated(DOMAIN)
vol.All(
# Deprecated in Home Assistant 2021.6
cv.deprecated(DOMAIN),
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_POLLING, default=False): cv.boolean,
}
)
},
),
extra=vol.ALLOW_EXTRA,
)
CHANGE_SETTING_SCHEMA = vol.Schema( CHANGE_SETTING_SCHEMA = vol.Schema(
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string} {vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
@ -92,22 +75,6 @@ class AbodeSystem:
self.logout_listener = None self.logout_listener = None
async def async_setup(hass, config):
"""Set up Abode integration."""
if DOMAIN not in config:
return True
conf = config[DOMAIN]
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=deepcopy(conf)
)
)
return True
async def async_setup_entry(hass, config_entry): async def async_setup_entry(hass, config_entry):
"""Set up Abode integration from a config entry.""" """Set up Abode integration from a config entry."""
username = config_entry.data.get(CONF_USERNAME) username = config_entry.data.get(CONF_USERNAME)
@ -284,17 +251,13 @@ class AbodeEntity(Entity):
"""Initialize Abode entity.""" """Initialize Abode entity."""
self._data = data self._data = data
self._available = True self._available = True
self._attr_should_poll = data.polling
@property @property
def available(self): def available(self):
"""Return the available state.""" """Return the available state."""
return self._available return self._available
@property
def should_poll(self):
"""Return the polling state."""
return self._data.polling
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe to Abode connection status updates.""" """Subscribe to Abode connection status updates."""
await self.hass.async_add_executor_job( await self.hass.async_add_executor_job(
@ -324,6 +287,8 @@ class AbodeDevice(AbodeEntity):
"""Initialize Abode device.""" """Initialize Abode device."""
super().__init__(data) super().__init__(data)
self._device = device self._device = device
self._attr_name = device.name
self._attr_unique_id = device.device_uuid
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe to device events.""" """Subscribe to device events."""
@ -345,11 +310,6 @@ class AbodeDevice(AbodeEntity):
"""Update device state.""" """Update device state."""
self._device.refresh() self._device.refresh()
@property
def name(self):
"""Return the name of the device."""
return self._device.name
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
@ -361,11 +321,6 @@ class AbodeDevice(AbodeEntity):
"device_type": self._device.type, "device_type": self._device.type,
} }
@property
def unique_id(self):
"""Return a unique ID to use for this device."""
return self._device.device_uuid
@property @property
def device_info(self): def device_info(self):
"""Return device registry information for this entity.""" """Return device registry information for this entity."""
@ -388,22 +343,13 @@ class AbodeAutomation(AbodeEntity):
"""Initialize for Abode automation.""" """Initialize for Abode automation."""
super().__init__(data) super().__init__(data)
self._automation = automation self._automation = automation
self._attr_name = automation.name
self._attr_unique_id = automation.automation_id
self._attr_extra_state_attributes = {
ATTR_ATTRIBUTION: ATTRIBUTION,
"type": "CUE automation",
}
def update(self): def update(self):
"""Update automation state.""" """Update automation state."""
self._automation.refresh() self._automation.refresh()
@property
def name(self):
"""Return the name of the automation."""
return self._automation.name
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION, "type": "CUE automation"}
@property
def unique_id(self):
"""Return a unique ID to use for this automation."""
return self._automation.automation_id

View File

@ -28,10 +28,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity): class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
"""An alarm_control_panel implementation for Abode.""" """An alarm_control_panel implementation for Abode."""
@property _attr_icon = ICON
def icon(self): _attr_code_arm_required = False
"""Return the icon.""" _attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY
return ICON
@property @property
def state(self): def state(self):
@ -46,16 +45,6 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
state = None state = None
return state return state
@property
def code_arm_required(self):
"""Whether the code is required for arm actions."""
return False
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
self._device.set_standby() self._device.set_standby()

View File

@ -158,13 +158,3 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._password = user_input[CONF_PASSWORD] self._password = user_input[CONF_PASSWORD]
return await self._async_abode_login(step_id="reauth_confirm") return await self._async_abode_login(step_id="reauth_confirm")
async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
if self._async_current_entries():
LOGGER.warning("Already configured; Only a single configuration possible")
return self.async_abort(reason="single_instance_allowed")
self._polling = import_config.get(CONF_POLLING, False)
return await self.async_step_user(import_config)

View File

@ -41,23 +41,15 @@ class AbodeSensor(AbodeDevice, SensorEntity):
"""Initialize a sensor for an Abode device.""" """Initialize a sensor for an Abode device."""
super().__init__(data, device) super().__init__(data, device)
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._name = f"{self._device.name} {SENSOR_TYPES[self._sensor_type][0]}" self._attr_name = f"{device.name} {SENSOR_TYPES[sensor_type][0]}"
self._device_class = SENSOR_TYPES[self._sensor_type][1] self._attr_device_class = SENSOR_TYPES[self._sensor_type][1]
self._attr_unique_id = f"{device.device_uuid}-{sensor_type}"
@property if self._sensor_type == CONST.TEMP_STATUS_KEY:
def name(self): self._attr_unit_of_measurement = device.temp_unit
"""Return the name of the sensor.""" elif self._sensor_type == CONST.HUMI_STATUS_KEY:
return self._name self._attr_unit_of_measurement = device.humidity_unit
elif self._sensor_type == CONST.LUX_STATUS_KEY:
@property self._attr_unit_of_measurement = device.lux_unit
def device_class(self):
"""Return the device class."""
return self._device_class
@property
def unique_id(self):
"""Return a unique ID to use for this device."""
return f"{self._device.device_uuid}-{self._sensor_type}"
@property @property
def state(self): def state(self):
@ -68,13 +60,3 @@ class AbodeSensor(AbodeDevice, SensorEntity):
return self._device.humidity return self._device.humidity
if self._sensor_type == CONST.LUX_STATUS_KEY: if self._sensor_type == CONST.LUX_STATUS_KEY:
return self._device.lux return self._device.lux
@property
def unit_of_measurement(self):
"""Return the units of measurement."""
if self._sensor_type == CONST.TEMP_STATUS_KEY:
return self._device.temp_unit
if self._sensor_type == CONST.HUMI_STATUS_KEY:
return self._device.humidity_unit
if self._sensor_type == CONST.LUX_STATUS_KEY:
return self._device.lux_unit

View File

@ -48,6 +48,8 @@ class AbodeSwitch(AbodeDevice, SwitchEntity):
class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity): class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity):
"""A switch implementation for Abode automations.""" """A switch implementation for Abode automations."""
_attr_icon = ICON
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Set up trigger automation service.""" """Set up trigger automation service."""
await super().async_added_to_hass() await super().async_added_to_hass()
@ -73,8 +75,3 @@ class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity):
def is_on(self): def is_on(self):
"""Return True if the automation is enabled.""" """Return True if the automation is enabled."""
return self._automation.is_enabled return self._automation.is_enabled
@property
def icon(self):
"""Return the robot icon to match Home Assistant automations."""
return ICON

View File

@ -26,7 +26,7 @@
"user": { "user": {
"data": { "data": {
"password": "Passwort", "password": "Passwort",
"username": "E-Mail-Adresse" "username": "E-Mail"
}, },
"title": "Gib deine Abode-Anmeldeinformationen ein" "title": "Gib deine Abode-Anmeldeinformationen ein"
} }

View File

@ -1,9 +1,12 @@
{ {
"config": { "config": {
"abort": { "abort": {
"reauth_successful": "La reautenticaci\u00f3n fue exitosa",
"single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode." "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode."
}, },
"error": { "error": {
"cannot_connect": "No se pudo conectar",
"invalid_auth": "Autenticaci\u00f3n inv\u00e1lida",
"invalid_mfa_code": "C\u00f3digo MFA no v\u00e1lido" "invalid_mfa_code": "C\u00f3digo MFA no v\u00e1lido"
}, },
"step": { "step": {
@ -15,7 +18,8 @@
}, },
"reauth_confirm": { "reauth_confirm": {
"data": { "data": {
"password": "Contrase\u00f1a" "password": "Contrase\u00f1a",
"username": "Correo electr\u00f3nico"
}, },
"title": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode" "title": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode"
}, },

View File

@ -20,7 +20,8 @@
"data": { "data": {
"password": "Jelsz\u00f3", "password": "Jelsz\u00f3",
"username": "E-mail" "username": "E-mail"
} },
"title": "T\u00f6ltse ki az Abode bejelentkez\u00e9si adatait"
}, },
"user": { "user": {
"data": { "data": {

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Final from typing import Final
from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY, ATTR_CONDITION_CLOUDY,
@ -21,8 +21,6 @@ from homeassistant.components.weather import (
ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY,
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
CONCENTRATION_PARTS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_CUBIC_METER,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
LENGTH_FEET, LENGTH_FEET,
@ -38,16 +36,12 @@ from homeassistant.const import (
UV_INDEX, UV_INDEX,
) )
from .model import SensorDescription from .model import AccuWeatherSensorDescription
API_IMPERIAL: Final = "Imperial" API_IMPERIAL: Final = "Imperial"
API_METRIC: Final = "Metric" API_METRIC: Final = "Metric"
ATTRIBUTION: Final = "Data provided by AccuWeather" ATTRIBUTION: Final = "Data provided by AccuWeather"
ATTR_ENABLED: Final = "enabled"
ATTR_FORECAST: Final = "forecast" ATTR_FORECAST: Final = "forecast"
ATTR_LABEL: Final = "label"
ATTR_UNIT_IMPERIAL: Final = "unit_imperial"
ATTR_UNIT_METRIC: Final = "unit_metric"
CONF_FORECAST: Final = "forecast" CONF_FORECAST: Final = "forecast"
DOMAIN: Final = "accuweather" DOMAIN: Final = "accuweather"
MANUFACTURER: Final = "AccuWeather, Inc." MANUFACTURER: Final = "AccuWeather, Inc."
@ -71,276 +65,263 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = {
ATTR_CONDITION_WINDY: [32], ATTR_CONDITION_WINDY: [32],
} }
FORECAST_SENSOR_TYPES: Final[dict[str, SensorDescription]] = { FORECAST_SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = (
"CloudCoverDay": { AccuWeatherSensorDescription(
ATTR_DEVICE_CLASS: None, key="CloudCoverDay",
ATTR_ICON: "mdi:weather-cloudy", icon="mdi:weather-cloudy",
ATTR_LABEL: "Cloud Cover Day", name="Cloud Cover Day",
ATTR_UNIT_METRIC: PERCENTAGE, unit_metric=PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, unit_imperial=PERCENTAGE,
ATTR_ENABLED: False, entity_registry_enabled_default=False,
}, ),
"CloudCoverNight": { AccuWeatherSensorDescription(
ATTR_DEVICE_CLASS: None, key="CloudCoverNight",
ATTR_ICON: "mdi:weather-cloudy", icon="mdi:weather-cloudy",
ATTR_LABEL: "Cloud Cover Night", name="Cloud Cover Night",
ATTR_UNIT_METRIC: PERCENTAGE, unit_metric=PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, unit_imperial=PERCENTAGE,
ATTR_ENABLED: False, entity_registry_enabled_default=False,
}, ),
"Grass": { AccuWeatherSensorDescription(
ATTR_DEVICE_CLASS: None, key="Grass",
ATTR_ICON: "mdi:grass", icon="mdi:grass",
ATTR_LABEL: "Grass Pollen", name="Grass Pollen",
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_ENABLED: False, entity_registry_enabled_default=False,
}, ),
"HoursOfSun": { AccuWeatherSensorDescription(
ATTR_DEVICE_CLASS: None, key="HoursOfSun",
ATTR_ICON: "mdi:weather-partly-cloudy", icon="mdi:weather-partly-cloudy",
ATTR_LABEL: "Hours Of Sun", name="Hours Of Sun",
ATTR_UNIT_METRIC: TIME_HOURS, unit_metric=TIME_HOURS,
ATTR_UNIT_IMPERIAL: TIME_HOURS, unit_imperial=TIME_HOURS,
ATTR_ENABLED: True, ),
}, AccuWeatherSensorDescription(
"Mold": { key="Mold",
ATTR_DEVICE_CLASS: None, icon="mdi:blur",
ATTR_ICON: "mdi:blur", name="Mold Pollen",
ATTR_LABEL: "Mold Pollen", unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False,
ATTR_ENABLED: False, ),
}, AccuWeatherSensorDescription(
"Ozone": { key="Ozone",
ATTR_DEVICE_CLASS: None, icon="mdi:vector-triangle",
ATTR_ICON: "mdi:vector-triangle", name="Ozone",
ATTR_LABEL: "Ozone", unit_metric=None,
ATTR_UNIT_METRIC: None, unit_imperial=None,
ATTR_UNIT_IMPERIAL: None, entity_registry_enabled_default=False,
ATTR_ENABLED: False, ),
}, AccuWeatherSensorDescription(
"Ragweed": { key="Ragweed",
ATTR_DEVICE_CLASS: None, icon="mdi:sprout",
ATTR_ICON: "mdi:sprout", name="Ragweed Pollen",
ATTR_LABEL: "Ragweed Pollen", unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, entity_registry_enabled_default=False,
ATTR_ENABLED: False, ),
}, AccuWeatherSensorDescription(
"RealFeelTemperatureMax": { key="RealFeelTemperatureMax",
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, name="RealFeel Temperature Max",
ATTR_LABEL: "RealFeel Temperature Max", unit_metric=TEMP_CELSIUS,
ATTR_UNIT_METRIC: TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, ),
ATTR_ENABLED: True, AccuWeatherSensorDescription(
}, key="RealFeelTemperatureMin",
"RealFeelTemperatureMin": { device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, name="RealFeel Temperature Min",
ATTR_ICON: None, unit_metric=TEMP_CELSIUS,
ATTR_LABEL: "RealFeel Temperature Min", unit_imperial=TEMP_FAHRENHEIT,
ATTR_UNIT_METRIC: TEMP_CELSIUS, ),
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, AccuWeatherSensorDescription(
ATTR_ENABLED: True, key="RealFeelTemperatureShadeMax",
}, device_class=DEVICE_CLASS_TEMPERATURE,
"RealFeelTemperatureShadeMax": { name="RealFeel Temperature Shade Max",
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, unit_metric=TEMP_CELSIUS,
ATTR_ICON: None, unit_imperial=TEMP_FAHRENHEIT,
ATTR_LABEL: "RealFeel Temperature Shade Max", entity_registry_enabled_default=False,
ATTR_UNIT_METRIC: TEMP_CELSIUS, ),
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, AccuWeatherSensorDescription(
ATTR_ENABLED: False, key="RealFeelTemperatureShadeMin",
}, device_class=DEVICE_CLASS_TEMPERATURE,
"RealFeelTemperatureShadeMin": { name="RealFeel Temperature Shade Min",
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, unit_metric=TEMP_CELSIUS,
ATTR_ICON: None, unit_imperial=TEMP_FAHRENHEIT,
ATTR_LABEL: "RealFeel Temperature Shade Min", entity_registry_enabled_default=False,
ATTR_UNIT_METRIC: TEMP_CELSIUS, ),
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, AccuWeatherSensorDescription(
ATTR_ENABLED: False, key="ThunderstormProbabilityDay",
}, icon="mdi:weather-lightning",
"ThunderstormProbabilityDay": { name="Thunderstorm Probability Day",
ATTR_DEVICE_CLASS: None, unit_metric=PERCENTAGE,
ATTR_ICON: "mdi:weather-lightning", unit_imperial=PERCENTAGE,
ATTR_LABEL: "Thunderstorm Probability Day", ),
ATTR_UNIT_METRIC: PERCENTAGE, AccuWeatherSensorDescription(
ATTR_UNIT_IMPERIAL: PERCENTAGE, key="ThunderstormProbabilityNight",
ATTR_ENABLED: True, icon="mdi:weather-lightning",
}, name="Thunderstorm Probability Night",
"ThunderstormProbabilityNight": { unit_metric=PERCENTAGE,
ATTR_DEVICE_CLASS: None, unit_imperial=PERCENTAGE,
ATTR_ICON: "mdi:weather-lightning", ),
ATTR_LABEL: "Thunderstorm Probability Night", AccuWeatherSensorDescription(
ATTR_UNIT_METRIC: PERCENTAGE, key="Tree",
ATTR_UNIT_IMPERIAL: PERCENTAGE, icon="mdi:tree-outline",
ATTR_ENABLED: True, name="Tree Pollen",
}, unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER,
"Tree": { unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER,
ATTR_DEVICE_CLASS: None, entity_registry_enabled_default=False,
ATTR_ICON: "mdi:tree-outline", ),
ATTR_LABEL: "Tree Pollen", AccuWeatherSensorDescription(
ATTR_UNIT_METRIC: CONCENTRATION_PARTS_PER_CUBIC_METER, key="UVIndex",
ATTR_UNIT_IMPERIAL: CONCENTRATION_PARTS_PER_CUBIC_METER, icon="mdi:weather-sunny",
ATTR_ENABLED: False, name="UV Index",
}, unit_metric=UV_INDEX,
"UVIndex": { unit_imperial=UV_INDEX,
ATTR_DEVICE_CLASS: None, ),
ATTR_ICON: "mdi:weather-sunny", AccuWeatherSensorDescription(
ATTR_LABEL: "UV Index", key="WindGustDay",
ATTR_UNIT_METRIC: UV_INDEX, icon="mdi:weather-windy",
ATTR_UNIT_IMPERIAL: UV_INDEX, name="Wind Gust Day",
ATTR_ENABLED: True, unit_metric=SPEED_KILOMETERS_PER_HOUR,
}, unit_imperial=SPEED_MILES_PER_HOUR,
"WindGustDay": { entity_registry_enabled_default=False,
ATTR_DEVICE_CLASS: None, ),
ATTR_ICON: "mdi:weather-windy", AccuWeatherSensorDescription(
ATTR_LABEL: "Wind Gust Day", key="WindGustNight",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy",
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, name="Wind Gust Night",
ATTR_ENABLED: False, unit_metric=SPEED_KILOMETERS_PER_HOUR,
}, unit_imperial=SPEED_MILES_PER_HOUR,
"WindGustNight": { entity_registry_enabled_default=False,
ATTR_DEVICE_CLASS: None, ),
ATTR_ICON: "mdi:weather-windy", AccuWeatherSensorDescription(
ATTR_LABEL: "Wind Gust Night", key="WindDay",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, icon="mdi:weather-windy",
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, name="Wind Day",
ATTR_ENABLED: False, unit_metric=SPEED_KILOMETERS_PER_HOUR,
}, unit_imperial=SPEED_MILES_PER_HOUR,
"WindDay": { ),
ATTR_DEVICE_CLASS: None, AccuWeatherSensorDescription(
ATTR_ICON: "mdi:weather-windy", key="WindNight",
ATTR_LABEL: "Wind Day", icon="mdi:weather-windy",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, name="Wind Night",
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, unit_metric=SPEED_KILOMETERS_PER_HOUR,
ATTR_ENABLED: True, unit_imperial=SPEED_MILES_PER_HOUR,
}, ),
"WindNight": { )
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-windy",
ATTR_LABEL: "Wind Night",
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR,
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
ATTR_ENABLED: True,
},
}
SENSOR_TYPES: Final[dict[str, SensorDescription]] = { SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = (
"ApparentTemperature": { AccuWeatherSensorDescription(
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, key="ApparentTemperature",
ATTR_ICON: None, device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_LABEL: "Apparent Temperature", name="Apparent Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS, unit_metric=TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, unit_imperial=TEMP_FAHRENHEIT,
ATTR_ENABLED: False, entity_registry_enabled_default=False,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
}, ),
"Ceiling": { AccuWeatherSensorDescription(
ATTR_DEVICE_CLASS: None, key="Ceiling",
ATTR_ICON: "mdi:weather-fog", icon="mdi:weather-fog",
ATTR_LABEL: "Cloud Ceiling", name="Cloud Ceiling",
ATTR_UNIT_METRIC: LENGTH_METERS, unit_metric=LENGTH_METERS,
ATTR_UNIT_IMPERIAL: LENGTH_FEET, unit_imperial=LENGTH_FEET,
ATTR_ENABLED: True, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, AccuWeatherSensorDescription(
"CloudCover": { key="CloudCover",
ATTR_DEVICE_CLASS: None, icon="mdi:weather-cloudy",
ATTR_ICON: "mdi:weather-cloudy", name="Cloud Cover",
ATTR_LABEL: "Cloud Cover", unit_metric=PERCENTAGE,
ATTR_UNIT_METRIC: PERCENTAGE, unit_imperial=PERCENTAGE,
ATTR_UNIT_IMPERIAL: PERCENTAGE, entity_registry_enabled_default=False,
ATTR_ENABLED: False, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, AccuWeatherSensorDescription(
"DewPoint": { key="DewPoint",
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, name="Dew Point",
ATTR_LABEL: "Dew Point", unit_metric=TEMP_CELSIUS,
ATTR_UNIT_METRIC: TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, entity_registry_enabled_default=False,
ATTR_ENABLED: False, state_class=STATE_CLASS_MEASUREMENT,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ),
}, AccuWeatherSensorDescription(
"RealFeelTemperature": { key="RealFeelTemperature",
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None, name="RealFeel Temperature",
ATTR_LABEL: "RealFeel Temperature", unit_metric=TEMP_CELSIUS,
ATTR_UNIT_METRIC: TEMP_CELSIUS, unit_imperial=TEMP_FAHRENHEIT,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, state_class=STATE_CLASS_MEASUREMENT,
ATTR_ENABLED: True, ),
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, AccuWeatherSensorDescription(
}, key="RealFeelTemperatureShade",
"RealFeelTemperatureShade": { device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, name="RealFeel Temperature Shade",
ATTR_ICON: None, unit_metric=TEMP_CELSIUS,
ATTR_LABEL: "RealFeel Temperature Shade", unit_imperial=TEMP_FAHRENHEIT,
ATTR_UNIT_METRIC: TEMP_CELSIUS, entity_registry_enabled_default=False,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, state_class=STATE_CLASS_MEASUREMENT,
ATTR_ENABLED: False, ),
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, AccuWeatherSensorDescription(
}, key="Precipitation",
"Precipitation": { icon="mdi:weather-rainy",
ATTR_DEVICE_CLASS: None, name="Precipitation",
ATTR_ICON: "mdi:weather-rainy", unit_metric=LENGTH_MILLIMETERS,
ATTR_LABEL: "Precipitation", unit_imperial=LENGTH_INCHES,
ATTR_UNIT_METRIC: LENGTH_MILLIMETERS, state_class=STATE_CLASS_MEASUREMENT,
ATTR_UNIT_IMPERIAL: LENGTH_INCHES, ),
ATTR_ENABLED: True, AccuWeatherSensorDescription(
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, key="PressureTendency",
}, device_class="accuweather__pressure_tendency",
"PressureTendency": { icon="mdi:gauge",
ATTR_DEVICE_CLASS: "accuweather__pressure_tendency", name="Pressure Tendency",
ATTR_ICON: "mdi:gauge", unit_metric=None,
ATTR_LABEL: "Pressure Tendency", unit_imperial=None,
ATTR_UNIT_METRIC: None, ),
ATTR_UNIT_IMPERIAL: None, AccuWeatherSensorDescription(
ATTR_ENABLED: True, key="UVIndex",
}, icon="mdi:weather-sunny",
"UVIndex": { name="UV Index",
ATTR_DEVICE_CLASS: None, unit_metric=UV_INDEX,
ATTR_ICON: "mdi:weather-sunny", unit_imperial=UV_INDEX,
ATTR_LABEL: "UV Index", state_class=STATE_CLASS_MEASUREMENT,
ATTR_UNIT_METRIC: UV_INDEX, ),
ATTR_UNIT_IMPERIAL: UV_INDEX, AccuWeatherSensorDescription(
ATTR_ENABLED: True, key="WetBulbTemperature",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, device_class=DEVICE_CLASS_TEMPERATURE,
}, name="Wet Bulb Temperature",
"WetBulbTemperature": { unit_metric=TEMP_CELSIUS,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, unit_imperial=TEMP_FAHRENHEIT,
ATTR_ICON: None, entity_registry_enabled_default=False,
ATTR_LABEL: "Wet Bulb Temperature", state_class=STATE_CLASS_MEASUREMENT,
ATTR_UNIT_METRIC: TEMP_CELSIUS, ),
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, AccuWeatherSensorDescription(
ATTR_ENABLED: False, key="WindChillTemperature",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, device_class=DEVICE_CLASS_TEMPERATURE,
}, name="Wind Chill Temperature",
"WindChillTemperature": { unit_metric=TEMP_CELSIUS,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, unit_imperial=TEMP_FAHRENHEIT,
ATTR_ICON: None, entity_registry_enabled_default=False,
ATTR_LABEL: "Wind Chill Temperature", state_class=STATE_CLASS_MEASUREMENT,
ATTR_UNIT_METRIC: TEMP_CELSIUS, ),
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, AccuWeatherSensorDescription(
ATTR_ENABLED: False, key="Wind",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, icon="mdi:weather-windy",
}, name="Wind",
"Wind": { unit_metric=SPEED_KILOMETERS_PER_HOUR,
ATTR_DEVICE_CLASS: None, unit_imperial=SPEED_MILES_PER_HOUR,
ATTR_ICON: "mdi:weather-windy", state_class=STATE_CLASS_MEASUREMENT,
ATTR_LABEL: "Wind", ),
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, AccuWeatherSensorDescription(
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, key="WindGust",
ATTR_ENABLED: True, icon="mdi:weather-windy",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, name="Wind Gust",
}, unit_metric=SPEED_KILOMETERS_PER_HOUR,
"WindGust": { unit_imperial=SPEED_MILES_PER_HOUR,
ATTR_DEVICE_CLASS: None, entity_registry_enabled_default=False,
ATTR_ICON: "mdi:weather-windy", state_class=STATE_CLASS_MEASUREMENT,
ATTR_LABEL: "Wind Gust", ),
ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, )
ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR,
ATTR_ENABLED: False,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
}

View File

@ -1,16 +1,14 @@
"""Type definitions for AccuWeather integration.""" """Type definitions for AccuWeather integration."""
from __future__ import annotations from __future__ import annotations
from typing import TypedDict from dataclasses import dataclass
from homeassistant.components.sensor import SensorEntityDescription
class SensorDescription(TypedDict, total=False): @dataclass
"""Sensor description class.""" class AccuWeatherSensorDescription(SensorEntityDescription):
"""Class describing AccuWeather sensor entities."""
device_class: str | None unit_metric: str | None = None
icon: str | None unit_imperial: str | None = None
label: str
unit_metric: str | None
unit_imperial: str | None
enabled: bool
state_class: str | None

View File

@ -3,17 +3,10 @@ from __future__ import annotations
from typing import Any, cast from typing import Any, cast
from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TEMPERATURE
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
ATTR_ICON,
CONF_NAME,
DEVICE_CLASS_TEMPERATURE,
)
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -22,11 +15,7 @@ from . import AccuWeatherDataUpdateCoordinator
from .const import ( from .const import (
API_IMPERIAL, API_IMPERIAL,
API_METRIC, API_METRIC,
ATTR_ENABLED,
ATTR_FORECAST, ATTR_FORECAST,
ATTR_LABEL,
ATTR_UNIT_IMPERIAL,
ATTR_UNIT_METRIC,
ATTRIBUTION, ATTRIBUTION,
DOMAIN, DOMAIN,
FORECAST_SENSOR_TYPES, FORECAST_SENSOR_TYPES,
@ -35,6 +24,7 @@ from .const import (
NAME, NAME,
SENSOR_TYPES, SENSOR_TYPES,
) )
from .model import AccuWeatherSensorDescription
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@ -48,17 +38,19 @@ async def async_setup_entry(
coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
sensors: list[AccuWeatherSensor] = [] sensors: list[AccuWeatherSensor] = []
for sensor in SENSOR_TYPES: for description in SENSOR_TYPES:
sensors.append(AccuWeatherSensor(name, sensor, coordinator)) sensors.append(AccuWeatherSensor(name, coordinator, description))
if coordinator.forecast: if coordinator.forecast:
for sensor in FORECAST_SENSOR_TYPES: for description in FORECAST_SENSOR_TYPES:
for day in range(MAX_FORECAST_DAYS + 1): for day in range(MAX_FORECAST_DAYS + 1):
# Some air quality/allergy sensors are only available for certain # Some air quality/allergy sensors are only available for certain
# locations. # locations.
if sensor in coordinator.data[ATTR_FORECAST][0]: if description.key in coordinator.data[ATTR_FORECAST][0]:
sensors.append( sensors.append(
AccuWeatherSensor(name, sensor, coordinator, forecast_day=day) AccuWeatherSensor(
name, coordinator, description, forecast_day=day
)
) )
async_add_entities(sensors) async_add_entities(sensors)
@ -68,119 +60,107 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
"""Define an AccuWeather entity.""" """Define an AccuWeather entity."""
coordinator: AccuWeatherDataUpdateCoordinator coordinator: AccuWeatherDataUpdateCoordinator
entity_description: AccuWeatherSensorDescription
def __init__( def __init__(
self, self,
name: str, name: str,
kind: str,
coordinator: AccuWeatherDataUpdateCoordinator, coordinator: AccuWeatherDataUpdateCoordinator,
description: AccuWeatherSensorDescription,
forecast_day: int | None = None, forecast_day: int | None = None,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._sensor_data = _get_sensor_data(coordinator.data, forecast_day, kind) self.entity_description = description
if forecast_day is None: self._sensor_data = _get_sensor_data(
self._description = SENSOR_TYPES[kind] coordinator.data, forecast_day, description.key
else: )
self._description = FORECAST_SENSOR_TYPES[kind]
self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL
self._name = name
self.kind = kind
self._device_class = None
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
self.forecast_day = forecast_day if forecast_day is not None:
self._attr_state_class = self._description.get(ATTR_STATE_CLASS) self._attr_name = f"{name} {description.name} {forecast_day}d"
self._attr_unique_id = (
@property f"{coordinator.location_key}-{description.key}-{forecast_day}".lower()
def name(self) -> str: )
"""Return the name.""" else:
if self.forecast_day is not None: self._attr_name = f"{name} {description.name}"
return f"{self._name} {self._description[ATTR_LABEL]} {self.forecast_day}d" self._attr_unique_id = (
return f"{self._name} {self._description[ATTR_LABEL]}" f"{coordinator.location_key}-{description.key}".lower()
)
@property if coordinator.is_metric:
def unique_id(self) -> str: self._unit_system = API_METRIC
"""Return a unique_id for this entity.""" self._attr_unit_of_measurement = description.unit_metric
if self.forecast_day is not None: else:
return f"{self.coordinator.location_key}-{self.kind}-{self.forecast_day}".lower() self._unit_system = API_IMPERIAL
return f"{self.coordinator.location_key}-{self.kind}".lower() self._attr_unit_of_measurement = description.unit_imperial
self._attr_device_info = {
@property "identifiers": {(DOMAIN, coordinator.location_key)},
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return {
"identifiers": {(DOMAIN, self.coordinator.location_key)},
"name": NAME, "name": NAME,
"manufacturer": MANUFACTURER, "manufacturer": MANUFACTURER,
"entry_type": "service", "entry_type": "service",
} }
self.forecast_day = forecast_day
@property @property
def state(self) -> StateType: def state(self) -> StateType:
"""Return the state.""" """Return the state."""
if self.forecast_day is not None: if self.forecast_day is not None:
if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE: if self.entity_description.device_class == DEVICE_CLASS_TEMPERATURE:
return cast(float, self._sensor_data["Value"]) return cast(float, self._sensor_data["Value"])
if self.kind == "UVIndex": if self.entity_description.key == "UVIndex":
return cast(int, self._sensor_data["Value"]) return cast(int, self._sensor_data["Value"])
if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "Ozone"]: if self.entity_description.key in ("Grass", "Mold", "Ragweed", "Tree", "Ozone"):
return cast(int, self._sensor_data["Value"]) return cast(int, self._sensor_data["Value"])
if self.kind == "Ceiling": if self.entity_description.key == "Ceiling":
return round(self._sensor_data[self._unit_system]["Value"]) return round(self._sensor_data[self._unit_system]["Value"])
if self.kind == "PressureTendency": if self.entity_description.key == "PressureTendency":
return cast(str, self._sensor_data["LocalizedText"].lower()) return cast(str, self._sensor_data["LocalizedText"].lower())
if self._description["device_class"] == DEVICE_CLASS_TEMPERATURE: if self.entity_description.device_class == DEVICE_CLASS_TEMPERATURE:
return cast(float, self._sensor_data[self._unit_system]["Value"]) return cast(float, self._sensor_data[self._unit_system]["Value"])
if self.kind == "Precipitation": if self.entity_description.key == "Precipitation":
return cast(float, self._sensor_data[self._unit_system]["Value"]) return cast(float, self._sensor_data[self._unit_system]["Value"])
if self.kind in ["Wind", "WindGust"]: if self.entity_description.key in ("Wind", "WindGust"):
return cast(float, self._sensor_data["Speed"][self._unit_system]["Value"]) return cast(float, self._sensor_data["Speed"][self._unit_system]["Value"])
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: if self.entity_description.key in (
"WindDay",
"WindNight",
"WindGustDay",
"WindGustNight",
):
return cast(StateType, self._sensor_data["Speed"]["Value"]) return cast(StateType, self._sensor_data["Speed"]["Value"])
return cast(StateType, self._sensor_data) return cast(StateType, self._sensor_data)
@property
def icon(self) -> str | None:
"""Return the icon."""
return self._description[ATTR_ICON]
@property
def device_class(self) -> str | None:
"""Return the device_class."""
return self._description[ATTR_DEVICE_CLASS]
@property
def unit_of_measurement(self) -> str | None:
"""Return the unit the value is expressed in."""
if self.coordinator.is_metric:
return self._description[ATTR_UNIT_METRIC]
return self._description[ATTR_UNIT_IMPERIAL]
@property @property
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes.""" """Return the state attributes."""
if self.forecast_day is not None: if self.forecast_day is not None:
if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: if self.entity_description.key in (
"WindDay",
"WindNight",
"WindGustDay",
"WindGustNight",
):
self._attrs["direction"] = self._sensor_data["Direction"]["English"] self._attrs["direction"] = self._sensor_data["Direction"]["English"]
elif self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]: elif self.entity_description.key in (
"Grass",
"Mold",
"Ozone",
"Ragweed",
"Tree",
"UVIndex",
):
self._attrs["level"] = self._sensor_data["Category"] self._attrs["level"] = self._sensor_data["Category"]
return self._attrs return self._attrs
if self.kind == "UVIndex": if self.entity_description.key == "UVIndex":
self._attrs["level"] = self.coordinator.data["UVIndexText"] self._attrs["level"] = self.coordinator.data["UVIndexText"]
elif self.kind == "Precipitation": elif self.entity_description.key == "Precipitation":
self._attrs["type"] = self.coordinator.data["PrecipitationType"] self._attrs["type"] = self.coordinator.data["PrecipitationType"]
return self._attrs return self._attrs
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return self._description[ATTR_ENABLED]
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle data update.""" """Handle data update."""
self._sensor_data = _get_sensor_data( self._sensor_data = _get_sensor_data(
self.coordinator.data, self.forecast_day, self.kind self.coordinator.data, self.forecast_day, self.entity_description.key
) )
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -0,0 +1,30 @@
{
"config": {
"error": {
"requests_exceeded": "\u062a\u0645 \u062a\u062c\u0627\u0648\u0632 \u0627\u0644\u0639\u062f\u062f \u0627\u0644\u0645\u0633\u0645\u0648\u062d \u0628\u0647 \u0645\u0646 \u0627\u0644\u0637\u0644\u0628\u0627\u062a \u0625\u0644\u0649 Accuweather API. \u0639\u0644\u064a\u0643 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631 \u0623\u0648 \u062a\u063a\u064a\u064a\u0631 \u0645\u0641\u062a\u0627\u062d API."
},
"step": {
"user": {
"description": "\u0625\u0630\u0627 \u0643\u0646\u062a \u0628\u062d\u0627\u062c\u0629 \u0625\u0644\u0649 \u0645\u0633\u0627\u0639\u062f\u0629 \u0641\u064a \u0627\u0644\u062a\u0643\u0648\u064a\u0646 \u060c \u0641\u0642\u0645 \u0628\u0625\u0644\u0642\u0627\u0621 \u0646\u0638\u0631\u0629 \u0647\u0646\u0627: https://www.home-assistant.io/integrations/accuweather/ \n\n \u0644\u0627 \u064a\u062a\u0645 \u062a\u0645\u0643\u064a\u0646 \u0628\u0639\u0636 \u0623\u062c\u0647\u0632\u0629 \u0627\u0644\u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u0628\u0634\u0643\u0644 \u0627\u0641\u062a\u0631\u0627\u0636\u064a. \u064a\u0645\u0643\u0646\u0643 \u062a\u0645\u0643\u064a\u0646\u0647\u0645 \u0641\u064a \u0633\u062c\u0644 \u0627\u0644\u0643\u064a\u0627\u0646 \u0628\u0639\u062f \u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u062a\u0643\u0627\u0645\u0644.\n \u0644\u0627 \u064a\u062a\u0645 \u062a\u0645\u0643\u064a\u0646 \u062a\u0648\u0642\u0639\u0627\u062a \u0627\u0644\u0637\u0642\u0633 \u0627\u0641\u062a\u0631\u0627\u0636\u064a\u064b\u0627. \u064a\u0645\u0643\u0646\u0643 \u062a\u0645\u0643\u064a\u0646\u0647 \u0641\u064a \u062e\u064a\u0627\u0631\u0627\u062a \u0627\u0644\u062a\u0643\u0627\u0645\u0644.",
"title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "\u0627\u0644\u0646\u0634\u0631\u0629 \u0627\u0644\u062c\u0648\u064a\u0629"
},
"description": "\u0646\u0638\u0631\u064b\u0627 \u0644\u0642\u064a\u0648\u062f \u0627\u0644\u0625\u0635\u062f\u0627\u0631 \u0627\u0644\u0645\u062c\u0627\u0646\u064a \u0645\u0646 \u0645\u0641\u062a\u0627\u062d AccuWeather API \u060c \u0639\u0646\u062f \u062a\u0645\u0643\u064a\u0646 \u0627\u0644\u062a\u0646\u0628\u0624 \u0628\u0627\u0644\u0637\u0642\u0633 \u060c \u0633\u064a\u062a\u0645 \u0625\u062c\u0631\u0627\u0621 \u062a\u062d\u062f\u064a\u062b\u0627\u062a \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0643\u0644 80 \u062f\u0642\u064a\u0642\u0629 \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0643\u0644 40 \u062f\u0642\u064a\u0642\u0629.",
"title": "\u062e\u064a\u0627\u0631\u0627\u062a AccuWeather"
}
}
},
"system_health": {
"info": {
"can_reach_server": "\u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u062e\u0627\u062f\u0645 AccuWeather",
"remaining_requests": "\u0627\u0644\u0637\u0644\u0628\u0627\u062a \u0627\u0644\u0645\u062a\u0628\u0642\u064a\u0629 \u0627\u0644\u0645\u0633\u0645\u0648\u062d \u0628\u0647\u0627"
}
}
}

View File

@ -6,7 +6,7 @@
"error": { "error": {
"cannot_connect": "Verbindung fehlgeschlagen", "cannot_connect": "Verbindung fehlgeschlagen",
"invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel",
"requests_exceeded": "Die zul\u00e4ssige Anzahl von Anforderungen an die Accuweather-API wurde \u00fcberschritten. Sie m\u00fcssen warten oder den API-Schl\u00fcssel \u00e4ndern." "requests_exceeded": "Die zul\u00e4ssige Anzahl von Anforderungen an die Accuweather-API wurde \u00fcberschritten. Du musst warten oder den API-Schl\u00fcssel \u00e4ndern."
}, },
"step": { "step": {
"user": { "user": {

View File

@ -1,6 +1,11 @@
{ {
"config": { "config": {
"abort": {
"single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n."
},
"error": { "error": {
"cannot_connect": "No se pudo conectar",
"invalid_api_key": "Clave de API no v\u00e1lida",
"requests_exceeded": "Se super\u00f3 el n\u00famero permitido de solicitudes a la API de Accuweather. Tiene que esperar o cambiar la clave de API." "requests_exceeded": "Se super\u00f3 el n\u00famero permitido de solicitudes a la API de Accuweather. Tiene que esperar o cambiar la clave de API."
}, },
"step": { "step": {

View File

@ -14,8 +14,22 @@
"latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
"longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
"name": "\u05e9\u05dd" "name": "\u05e9\u05dd"
} },
} "title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"description": "\u05d1\u05e9\u05dc \u05de\u05d2\u05d1\u05dc\u05d5\u05ea \u05d4\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05d7\u05d9\u05e0\u05de\u05d9\u05ea \u05e9\u05dc \u05de\u05e4\u05ea\u05d7 \u05d4-API \u05e9\u05dc AccuWeather, \u05db\u05d0\u05e9\u05e8 \u05ea\u05e4\u05e2\u05d9\u05dc \u05ea\u05d7\u05d6\u05d9\u05ea \u05de\u05d6\u05d2 \u05d0\u05d5\u05d5\u05d9\u05e8, \u05e2\u05d3\u05db\u05d5\u05e0\u05d9 \u05e0\u05ea\u05d5\u05e0\u05d9\u05dd \u05d9\u05d1\u05d5\u05e6\u05e2\u05d5 \u05db\u05dc 80 \u05d3\u05e7\u05d5\u05ea \u05d1\u05de\u05e7\u05d5\u05dd \u05db\u05dc 40 \u05d3\u05e7\u05d5\u05ea.",
"title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea AccuWeather"
}
}
},
"system_health": {
"info": {
"can_reach_server": "\u05d4\u05e9\u05d2\u05ea \u05e9\u05e8\u05ea AccuWeather"
} }
} }
} }

View File

@ -25,5 +25,11 @@
"title": "AccuWeather be\u00e1ll\u00edt\u00e1sok" "title": "AccuWeather be\u00e1ll\u00edt\u00e1sok"
} }
} }
},
"system_health": {
"info": {
"can_reach_server": "\u00c9rje el az AccuWeather szervert",
"remaining_requests": "Fennmarad\u00f3 enged\u00e9lyezett k\u00e9r\u00e9sek"
}
} }
} }

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "\u0647\u0628\u0648\u0637",
"rising": "\u0627\u0631\u062a\u0641\u0627\u0639",
"steady": "\u062b\u0627\u0628\u062a"
}
}
}

View File

@ -19,7 +19,6 @@ from homeassistant.components.weather import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.dt import utc_from_timestamp
@ -60,29 +59,15 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._name = name self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL
self._unit_system = API_METRIC if self.coordinator.is_metric else API_IMPERIAL self._attr_name = name
self._attr_unique_id = coordinator.location_key
@property self._attr_temperature_unit = (
def name(self) -> str: TEMP_CELSIUS if coordinator.is_metric else TEMP_FAHRENHEIT
"""Return the name.""" )
return self._name self._attr_attribution = ATTRIBUTION
self._attr_device_info = {
@property "identifiers": {(DOMAIN, coordinator.location_key)},
def attribution(self) -> str:
"""Return the attribution."""
return ATTRIBUTION
@property
def unique_id(self) -> str:
"""Return a unique_id for this entity."""
return self.coordinator.location_key
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return {
"identifiers": {(DOMAIN, self.coordinator.location_key)},
"name": NAME, "name": NAME,
"manufacturer": MANUFACTURER, "manufacturer": MANUFACTURER,
"entry_type": "service", "entry_type": "service",
@ -107,11 +92,6 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
float, self.coordinator.data["Temperature"][self._unit_system]["Value"] float, self.coordinator.data["Temperature"][self._unit_system]["Value"]
) )
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT
@property @property
def pressure(self) -> float: def pressure(self) -> float:
"""Return the pressure.""" """Return the pressure."""

View File

@ -67,6 +67,8 @@ def setup_platform(
class AcerSwitch(SwitchEntity): class AcerSwitch(SwitchEntity):
"""Represents an Acer Projector as a switch.""" """Represents an Acer Projector as a switch."""
_attr_icon = ICON
def __init__( def __init__(
self, self,
serial_port: str, serial_port: str,
@ -79,9 +81,7 @@ class AcerSwitch(SwitchEntity):
port=serial_port, timeout=timeout, write_timeout=write_timeout port=serial_port, timeout=timeout, write_timeout=write_timeout
) )
self._serial_port = serial_port self._serial_port = serial_port
self._name = name self._attr_name = name
self._state = False
self._available = False
self._attributes = { self._attributes = {
LAMP_HOURS: STATE_UNKNOWN, LAMP_HOURS: STATE_UNKNOWN,
INPUT_SOURCE: STATE_UNKNOWN, INPUT_SOURCE: STATE_UNKNOWN,
@ -116,57 +116,33 @@ class AcerSwitch(SwitchEntity):
return match.group(1) return match.group(1)
return STATE_UNKNOWN return STATE_UNKNOWN
@property
def available(self) -> bool:
"""Return if projector is available."""
return self._available
@property
def name(self) -> str:
"""Return name of the projector."""
return self._name
@property
def icon(self) -> str:
"""Return the icon."""
return ICON
@property
def is_on(self) -> bool:
"""Return if the projector is turned on."""
return self._state
@property
def extra_state_attributes(self) -> dict[str, str]:
"""Return state attributes."""
return self._attributes
def update(self) -> None: def update(self) -> None:
"""Get the latest state from the projector.""" """Get the latest state from the projector."""
awns = self._write_read_format(CMD_DICT[LAMP]) awns = self._write_read_format(CMD_DICT[LAMP])
if awns == "Lamp 1": if awns == "Lamp 1":
self._state = True self._attr_is_on = True
self._available = True self._attr_available = True
elif awns == "Lamp 0": elif awns == "Lamp 0":
self._state = False self._attr_is_on = False
self._available = True self._attr_available = True
else: else:
self._available = False self._attr_available = False
for key in self._attributes: for key in self._attributes:
msg = CMD_DICT.get(key) msg = CMD_DICT.get(key)
if msg: if msg:
awns = self._write_read_format(msg) awns = self._write_read_format(msg)
self._attributes[key] = awns self._attributes[key] = awns
self._attr_extra_state_attributes = self._attributes
def turn_on(self, **kwargs: Any) -> None: def turn_on(self, **kwargs: Any) -> None:
"""Turn the projector on.""" """Turn the projector on."""
msg = CMD_DICT[STATE_ON] msg = CMD_DICT[STATE_ON]
self._write_read(msg) self._write_read(msg)
self._state = True self._attr_is_on = True
def turn_off(self, **kwargs: Any) -> None: def turn_off(self, **kwargs: Any) -> None:
"""Turn the projector off.""" """Turn the projector off."""
msg = CMD_DICT[STATE_OFF] msg = CMD_DICT[STATE_OFF]
self._write_read(msg) self._write_read(msg)
self._state = False self._attr_is_on = False

View File

@ -0,0 +1,18 @@
"""The Adax integration."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
PLATFORMS = ["climate"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Adax from a config entry."""
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,152 @@
"""Support for Adax wifi-enabled home heaters."""
from __future__ import annotations
import logging
from typing import Any
from adax import Adax
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_PASSWORD,
PRECISION_WHOLE,
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ACCOUNT_ID
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Adax thermostat with config flow."""
adax_data_handler = Adax(
entry.data[ACCOUNT_ID],
entry.data[CONF_PASSWORD],
websession=async_get_clientsession(hass),
)
async_add_entities(
AdaxDevice(room, adax_data_handler)
for room in await adax_data_handler.get_rooms()
)
class AdaxDevice(ClimateEntity):
"""Representation of a heater."""
def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None:
"""Initialize the heater."""
self._heater_data = heater_data
self._adax_data_handler = adax_data_handler
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_TARGET_TEMPERATURE
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return f"{self._heater_data['homeId']}_{self._heater_data['id']}"
@property
def name(self) -> str:
"""Return the name of the device, if any."""
return self._heater_data["name"]
@property
def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode."""
if self._heater_data["heatingEnabled"]:
return HVAC_MODE_HEAT
return HVAC_MODE_OFF
@property
def icon(self) -> str:
"""Return nice icon for heater."""
if self.hvac_mode == HVAC_MODE_HEAT:
return "mdi:radiator"
return "mdi:radiator-off"
@property
def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return [HVAC_MODE_HEAT, HVAC_MODE_OFF]
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set hvac mode."""
if hvac_mode == HVAC_MODE_HEAT:
temperature = max(
self.min_temp, self._heater_data.get("targetTemperature", self.min_temp)
)
await self._adax_data_handler.set_room_target_temperature(
self._heater_data["id"], temperature, True
)
elif hvac_mode == HVAC_MODE_OFF:
await self._adax_data_handler.set_room_target_temperature(
self._heater_data["id"], self.min_temp, False
)
else:
return
await self._adax_data_handler.update()
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement which this device uses."""
return TEMP_CELSIUS
@property
def min_temp(self) -> int:
"""Return the minimum temperature."""
return 5
@property
def max_temp(self) -> int:
"""Return the maximum temperature."""
return 35
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._heater_data.get("temperature")
@property
def target_temperature(self) -> int | None:
"""Return the temperature we try to reach."""
return self._heater_data.get("targetTemperature")
@property
def target_temperature_step(self) -> int:
"""Return the supported step of target temperature."""
return PRECISION_WHOLE
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
await self._adax_data_handler.set_room_target_temperature(
self._heater_data["id"], temperature, True
)
async def async_update(self) -> None:
"""Get the latest data."""
for room in await self._adax_data_handler.get_rooms():
if room["id"] == self._heater_data["id"]:
self._heater_data = room
return

View File

@ -0,0 +1,73 @@
"""Config flow for Adax integration."""
from __future__ import annotations
import logging
from typing import Any
import adax
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ACCOUNT_ID, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{vol.Required(ACCOUNT_ID): int, vol.Required(CONF_PASSWORD): str}
)
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
"""Validate the user input allows us to connect."""
account_id = data[ACCOUNT_ID]
password = data[CONF_PASSWORD].replace(" ", "")
token = await adax.get_adax_token(
async_get_clientsession(hass), account_id, password
)
if token is None:
_LOGGER.info("Adax: Failed to login to retrieve token")
raise CannotConnect
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Adax."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
errors = {}
await self.async_set_unique_id(user_input[ACCOUNT_ID])
self._abort_if_unique_id_configured()
try:
await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
else:
return self.async_create_entry(
title=user_input[ACCOUNT_ID], data=user_input
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@ -0,0 +1,5 @@
"""Constants for the Adax integration."""
from typing import Final
ACCOUNT_ID: Final = "account_id"
DOMAIN: Final = "adax"

View File

@ -0,0 +1,13 @@
{
"domain": "adax",
"name": "Adax",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/adax",
"requirements": [
"adax==0.0.1"
],
"codeowners": [
"@danielhiversen"
],
"iot_class": "cloud_polling"
}

View File

@ -0,0 +1,18 @@
{
"config": {
"step": {
"user": {
"data": {
"account_id": "Account ID",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
},
"step": {
"user": {
"data": {
"account_id": "ID del compte",
"host": "Amfitri\u00f3",
"password": "Contrasenya"
}
}
}
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno"
},
"error": {
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
"invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed"
},
"step": {
"user": {
"data": {
"host": "Hostitel",
"password": "Heslo"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert"
},
"error": {
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_auth": "Ung\u00fcltige Authentifizierung"
},
"step": {
"user": {
"data": {
"account_id": "Konto-ID",
"host": "Host",
"password": "Passwort"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication"
},
"step": {
"user": {
"data": {
"account_id": "Account ID",
"host": "Host",
"password": "Password"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "Seade on juba h\u00e4\u00e4lestatud"
},
"error": {
"cannot_connect": "\u00dchendamine nurjus",
"invalid_auth": "Tuvastamise viga"
},
"step": {
"user": {
"data": {
"account_id": "Konto ID",
"host": "Host",
"password": "Salas\u00f5na"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9"
},
"error": {
"cannot_connect": "\u00c9chec de connexion",
"invalid_auth": "Authentification invalide"
},
"step": {
"user": {
"data": {
"account_id": "identifiant de compte",
"host": "H\u00f4te",
"password": "Mot de passe"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
},
"error": {
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
"invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9"
},
"step": {
"user": {
"data": {
"account_id": "\u05de\u05d6\u05d4\u05d4 \u05d7\u05e9\u05d1\u05d5\u05df",
"host": "\u05de\u05d0\u05e8\u05d7",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": {
"cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida"
},
"step": {
"user": {
"data": {
"account_id": "ID account",
"host": "Host",
"password": "Password"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "Apparaat is al geconfigureerd"
},
"error": {
"cannot_connect": "Kan geen verbinding maken",
"invalid_auth": "Ongeldige authenticatie"
},
"step": {
"user": {
"data": {
"account_id": "Account ID",
"host": "Host",
"password": "Wachtwoord"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"invalid_auth": "Niepoprawne uwierzytelnienie"
},
"step": {
"user": {
"data": {
"account_id": "Identyfikator konta",
"host": "Nazwa hosta lub adres IP",
"password": "Has\u0142o"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438."
},
"step": {
"user": {
"data": {
"account_id": "ID \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438",
"host": "\u0425\u043e\u0441\u0442",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
},
"step": {
"user": {
"data": {
"account_id": "\u5e33\u865f ID",
"host": "\u4e3b\u6a5f\u7aef",
"password": "\u5bc6\u78bc"
}
}
}
}
}

View File

@ -17,7 +17,7 @@
"host": "Host", "host": "Host",
"password": "Passwort", "password": "Passwort",
"port": "Port", "port": "Port",
"ssl": "AdGuard Home verwendet ein SSL-Zertifikat", "ssl": "Verwendet ein SSL-Zertifikat",
"username": "Benutzername", "username": "Benutzername",
"verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
}, },

View File

@ -1,9 +1,16 @@
{ {
"config": { "config": {
"abort": {
"already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
},
"error": { "error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s" "cannot_connect": "Sikertelen csatlakoz\u00e1s"
}, },
"step": { "step": {
"hassio_confirm": {
"description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant-ot, hogy csatlakozzon az AdGuard Home-hoz, amelyet a kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?",
"title": "Az AdGuard Home a Home Assistant kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel"
},
"user": { "user": {
"data": { "data": {
"host": "Hoszt", "host": "Hoszt",

View File

@ -1,6 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "Layanan sudah dikonfigurasi",
"existing_instance_updated": "Memperbarui konfigurasi yang ada." "existing_instance_updated": "Memperbarui konfigurasi yang ada."
}, },
"error": { "error": {

View File

@ -268,15 +268,17 @@ class AdsHub:
class AdsEntity(Entity): class AdsEntity(Entity):
"""Representation of ADS entity.""" """Representation of ADS entity."""
_attr_should_poll = False
def __init__(self, ads_hub, name, ads_var): def __init__(self, ads_hub, name, ads_var):
"""Initialize ADS binary sensor.""" """Initialize ADS binary sensor."""
self._name = name
self._unique_id = ads_var
self._state_dict = {} self._state_dict = {}
self._state_dict[STATE_KEY_STATE] = None self._state_dict[STATE_KEY_STATE] = None
self._ads_hub = ads_hub self._ads_hub = ads_hub
self._ads_var = ads_var self._ads_var = ads_var
self._event = None self._event = None
self._attr_unique_id = ads_var
self._attr_name = name
async def async_initialize_device( async def async_initialize_device(
self, ads_var, plctype, state_key=STATE_KEY_STATE, factor=None self, ads_var, plctype, state_key=STATE_KEY_STATE, factor=None
@ -311,21 +313,6 @@ class AdsEntity(Entity):
_LOGGER.debug("Variable %s: Timeout during first update", ads_var) _LOGGER.debug("Variable %s: Timeout during first update", ads_var)
@property @property
def name(self): def available(self) -> bool:
"""Return the default name of the binary sensor."""
return self._name
@property
def unique_id(self):
"""Return an unique identifier for this entity."""
return self._unique_id
@property
def should_poll(self):
"""Return False because entity pushes its state to HA."""
return False
@property
def available(self):
"""Return False if state has not been updated yet.""" """Return False if state has not been updated yet."""
return self._state_dict[STATE_KEY_STATE] is not None return self._state_dict[STATE_KEY_STATE] is not None

View File

@ -40,18 +40,13 @@ class AdsBinarySensor(AdsEntity, BinarySensorEntity):
def __init__(self, ads_hub, name, ads_var, device_class): def __init__(self, ads_hub, name, ads_var, device_class):
"""Initialize ADS binary sensor.""" """Initialize ADS binary sensor."""
super().__init__(ads_hub, name, ads_var) super().__init__(ads_hub, name, ads_var)
self._device_class = device_class or DEVICE_CLASS_MOVING self._attr_device_class = device_class or DEVICE_CLASS_MOVING
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register device notification.""" """Register device notification."""
await self.async_initialize_device(self._ads_var, self._ads_hub.PLCTYPE_BOOL) await self.async_initialize_device(self._ads_var, self._ads_hub.PLCTYPE_BOOL)
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return True if the entity is on.""" """Return True if the entity is on."""
return self._state_dict[STATE_KEY_STATE] return self._state_dict[STATE_KEY_STATE]
@property
def device_class(self):
"""Return the device class."""
return self._device_class

View File

@ -105,7 +105,12 @@ class AdsCover(AdsEntity, CoverEntity):
self._ads_var_open = ads_var_open self._ads_var_open = ads_var_open
self._ads_var_close = ads_var_close self._ads_var_close = ads_var_close
self._ads_var_stop = ads_var_stop self._ads_var_stop = ads_var_stop
self._device_class = device_class self._attr_device_class = device_class
self._attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
if ads_var_stop is not None:
self._attr_supported_features |= SUPPORT_STOP
if ads_var_pos_set is not None:
self._attr_supported_features |= SUPPORT_SET_POSITION
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register device notification.""" """Register device notification."""
@ -119,11 +124,6 @@ class AdsCover(AdsEntity, CoverEntity):
self._ads_var_position, self._ads_hub.PLCTYPE_BYTE, STATE_KEY_POSITION self._ads_var_position, self._ads_hub.PLCTYPE_BYTE, STATE_KEY_POSITION
) )
@property
def device_class(self):
"""Return the class of this cover."""
return self._device_class
@property @property
def is_closed(self): def is_closed(self):
"""Return if the cover is closed.""" """Return if the cover is closed."""
@ -138,19 +138,6 @@ class AdsCover(AdsEntity, CoverEntity):
"""Return current position of cover.""" """Return current position of cover."""
return self._state_dict[STATE_KEY_POSITION] return self._state_dict[STATE_KEY_POSITION]
@property
def supported_features(self):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
if self._ads_var_stop is not None:
supported_features |= SUPPORT_STOP
if self._ads_var_pos_set is not None:
supported_features |= SUPPORT_SET_POSITION
return supported_features
def stop_cover(self, **kwargs): def stop_cover(self, **kwargs):
"""Fire the stop action.""" """Fire the stop action."""
if self._ads_var_stop: if self._ads_var_stop:
@ -185,7 +172,7 @@ class AdsCover(AdsEntity, CoverEntity):
self.set_cover_position(position=0) self.set_cover_position(position=0)
@property @property
def available(self): def available(self) -> bool:
"""Return False if state has not been updated yet.""" """Return False if state has not been updated yet."""
if self._ads_var is not None or self._ads_var_position is not None: if self._ads_var is not None or self._ads_var_position is not None:
return ( return (

View File

@ -1,4 +1,6 @@
"""Support for ADS light sources.""" """Support for ADS light sources."""
from __future__ import annotations
import voluptuous as vol import voluptuous as vol
from homeassistant.components.light import ( from homeassistant.components.light import (
@ -48,6 +50,8 @@ class AdsLight(AdsEntity, LightEntity):
super().__init__(ads_hub, name, ads_var_enable) super().__init__(ads_hub, name, ads_var_enable)
self._state_dict[STATE_KEY_BRIGHTNESS] = None self._state_dict[STATE_KEY_BRIGHTNESS] = None
self._ads_var_brightness = ads_var_brightness self._ads_var_brightness = ads_var_brightness
if ads_var_brightness is not None:
self._attr_supported_features = SUPPORT_BRIGHTNESS
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register device notification.""" """Register device notification."""
@ -61,19 +65,12 @@ class AdsLight(AdsEntity, LightEntity):
) )
@property @property
def brightness(self): def brightness(self) -> int | None:
"""Return the brightness of the light (0..255).""" """Return the brightness of the light (0..255)."""
return self._state_dict[STATE_KEY_BRIGHTNESS] return self._state_dict[STATE_KEY_BRIGHTNESS]
@property @property
def supported_features(self): def is_on(self) -> bool:
"""Flag supported features."""
if self._ads_var_brightness is not None:
return SUPPORT_BRIGHTNESS
return 0
@property
def is_on(self):
"""Return True if the entity is on.""" """Return True if the entity is on."""
return self._state_dict[STATE_KEY_STATE] return self._state_dict[STATE_KEY_STATE]

View File

@ -5,6 +5,7 @@ from homeassistant.components import ads
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import StateType
from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, STATE_KEY_STATE, AdsEntity from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, STATE_KEY_STATE, AdsEntity
@ -49,7 +50,7 @@ class AdsSensor(AdsEntity, SensorEntity):
def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor): def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor):
"""Initialize AdsSensor entity.""" """Initialize AdsSensor entity."""
super().__init__(ads_hub, name, ads_var) super().__init__(ads_hub, name, ads_var)
self._unit_of_measurement = unit_of_measurement self._attr_unit_of_measurement = unit_of_measurement
self._ads_type = ads_type self._ads_type = ads_type
self._factor = factor self._factor = factor
@ -63,11 +64,6 @@ class AdsSensor(AdsEntity, SensorEntity):
) )
@property @property
def state(self): def state(self) -> StateType:
"""Return the state of the device.""" """Return the state of the device."""
return self._state_dict[STATE_KEY_STATE] return self._state_dict[STATE_KEY_STATE]
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement

View File

@ -35,7 +35,7 @@ class AdsSwitch(AdsEntity, SwitchEntity):
await self.async_initialize_device(self._ads_var, self._ads_hub.PLCTYPE_BOOL) await self.async_initialize_device(self._ads_var, self._ads_hub.PLCTYPE_BOOL)
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return True if the entity is on.""" """Return True if the entity is on."""
return self._state_dict[STATE_KEY_STATE] return self._state_dict[STATE_KEY_STATE]

View File

@ -35,15 +35,13 @@ class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity):
_attr_device_class = DEVICE_CLASS_PROBLEM _attr_device_class = DEVICE_CLASS_PROBLEM
@property def __init__(self, instance, ac_key):
def name(self): """Initialize an Advantage Air Filter."""
"""Return the name.""" super().__init__(instance, ac_key)
return f'{self._ac["name"]} Filter' self._attr_name = f'{self._ac["name"]} Filter'
self._attr_unique_id = (
@property f'{self.coordinator.data["system"]["rid"]}-{ac_key}-filter'
def unique_id(self): )
"""Return a unique id."""
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-filter'
@property @property
def is_on(self): def is_on(self):
@ -56,15 +54,13 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity):
_attr_device_class = DEVICE_CLASS_MOTION _attr_device_class = DEVICE_CLASS_MOTION
@property def __init__(self, instance, ac_key, zone_key):
def name(self): """Initialize an Advantage Air Zone Motion."""
"""Return the name.""" super().__init__(instance, ac_key, zone_key)
return f'{self._zone["name"]} Motion' self._attr_name = f'{self._zone["name"]} Motion'
self._attr_unique_id = (
@property f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-motion'
def unique_id(self): )
"""Return a unique id."""
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-motion'
@property @property
def is_on(self): def is_on(self):
@ -77,15 +73,13 @@ class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity):
_attr_entity_registry_enabled_default = False _attr_entity_registry_enabled_default = False
@property def __init__(self, instance, ac_key, zone_key):
def name(self): """Initialize an Advantage Air Zone MyZone."""
"""Return the name.""" super().__init__(instance, ac_key, zone_key)
return f'{self._zone["name"]} MyZone' self._attr_name = f'{self._zone["name"]} MyZone'
self._attr_unique_id = (
@property f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-myzone'
def unique_id(self): )
"""Return a unique id."""
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-myzone'
@property @property
def is_on(self): def is_on(self):

View File

@ -1,5 +1,4 @@
"""Climate platform for Advantage Air integration.""" """Climate platform for Advantage Air integration."""
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
FAN_AUTO, FAN_AUTO,
@ -16,6 +15,7 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
) )
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS
from homeassistant.core import callback
from homeassistant.helpers import entity_platform from homeassistant.helpers import entity_platform
from .const import ( from .const import (
@ -84,39 +84,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AdvantageAirClimateEntity(AdvantageAirEntity, ClimateEntity): class AdvantageAirClimateEntity(AdvantageAirEntity, ClimateEntity):
"""AdvantageAir Climate class.""" """AdvantageAir Climate class."""
@property _attr_temperature_unit = TEMP_CELSIUS
def temperature_unit(self): _attr_target_temperature_step = PRECISION_WHOLE
"""Return the temperature unit.""" _attr_max_temp = 32
return TEMP_CELSIUS _attr_min_temp = 16
@property
def target_temperature_step(self):
"""Return the supported temperature step."""
return PRECISION_WHOLE
@property
def max_temp(self):
"""Return the maximum supported temperature."""
return 32
@property
def min_temp(self):
"""Return the minimum supported temperature."""
return 16
class AdvantageAirAC(AdvantageAirClimateEntity): class AdvantageAirAC(AdvantageAirClimateEntity):
"""AdvantageAir AC unit.""" """AdvantageAir AC unit."""
@property _attr_fan_modes = [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH]
def name(self): _attr_hvac_modes = AC_HVAC_MODES
"""Return the name.""" _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
return self._ac["name"]
@property def __init__(self, instance, ac_key):
def unique_id(self): """Initialize an AdvantageAir AC unit."""
"""Return a unique id.""" super().__init__(instance, ac_key)
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}' self._attr_name = self._ac["name"]
self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{ac_key}'
if self._ac.get("myAutoModeEnabled"):
self._attr_hvac_modes = AC_HVAC_MODES + [HVAC_MODE_AUTO]
@property @property
def target_temperature(self): def target_temperature(self):
@ -130,28 +117,11 @@ class AdvantageAirAC(AdvantageAirClimateEntity):
return ADVANTAGE_AIR_HVAC_MODES.get(self._ac["mode"]) return ADVANTAGE_AIR_HVAC_MODES.get(self._ac["mode"])
return HVAC_MODE_OFF return HVAC_MODE_OFF
@property
def hvac_modes(self):
"""Return the supported HVAC modes."""
if self._ac.get("myAutoModeEnabled"):
return AC_HVAC_MODES + [HVAC_MODE_AUTO]
return AC_HVAC_MODES
@property @property
def fan_mode(self): def fan_mode(self):
"""Return the current fan modes.""" """Return the current fan modes."""
return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"]) return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"])
@property
def fan_modes(self):
"""Return the supported fan modes."""
return [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH]
@property
def supported_features(self):
"""Return the supported features."""
return SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
async def async_set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode):
"""Set the HVAC Mode and State.""" """Set the HVAC Mode and State."""
if hvac_mode == HVAC_MODE_OFF: if hvac_mode == HVAC_MODE_OFF:
@ -185,42 +155,30 @@ class AdvantageAirAC(AdvantageAirClimateEntity):
class AdvantageAirZone(AdvantageAirClimateEntity): class AdvantageAirZone(AdvantageAirClimateEntity):
"""AdvantageAir Zone control.""" """AdvantageAir Zone control."""
@property _attr_hvac_modes = ZONE_HVAC_MODES
def name(self): _attr_supported_features = SUPPORT_TARGET_TEMPERATURE
"""Return the name."""
return self._zone["name"]
@property def __init__(self, instance, ac_key, zone_key):
def unique_id(self): """Initialize an AdvantageAir Zone control."""
"""Return a unique id.""" super().__init__(instance, ac_key, zone_key)
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}' self._attr_name = self._zone["name"]
self._attr_unique_id = (
f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}'
)
@property async def async_added_to_hass(self):
def current_temperature(self): """When entity is added to hass."""
"""Return the current temperature.""" self.async_on_remove(self.coordinator.async_add_listener(self._update_callback))
return self._zone["measuredTemp"]
@property @callback
def target_temperature(self): def _update_callback(self) -> None:
"""Return the target temperature.""" """Load data from integration."""
return self._zone["setTemp"] self._attr_current_temperature = self._zone["measuredTemp"]
self._attr_target_temperature = self._zone["setTemp"]
@property self._attr_hvac_mode = HVAC_MODE_OFF
def hvac_mode(self):
"""Return the current HVAC modes."""
if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN:
return HVAC_MODE_FAN_ONLY self._attr_hvac_mode = HVAC_MODE_FAN_ONLY
return HVAC_MODE_OFF self.async_write_ha_state()
@property
def hvac_modes(self):
"""Return supported HVAC modes."""
return ZONE_HVAC_MODES
@property
def supported_features(self):
"""Return the supported features."""
return SUPPORT_TARGET_TEMPERATURE
async def async_set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode):
"""Set the HVAC Mode and State.""" """Set the HVAC Mode and State."""

View File

@ -36,25 +36,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity):
"""Advantage Air Cover Class.""" """Advantage Air Cover Class."""
@property _attr_device_class = DEVICE_CLASS_DAMPER
def name(self): _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
"""Return the name."""
return f'{self._zone["name"]}'
@property def __init__(self, instance, ac_key, zone_key):
def unique_id(self): """Initialize an Advantage Air Cover Class."""
"""Return a unique id.""" super().__init__(instance, ac_key, zone_key)
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}' self._attr_name = f'{self._zone["name"]}'
self._attr_unique_id = (
@property f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}'
def device_class(self): )
"""Return the device class of the vent."""
return DEVICE_CLASS_DAMPER
@property
def supported_features(self):
"""Return the supported features."""
return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
@property @property
def is_closed(self): def is_closed(self):

View File

@ -14,6 +14,13 @@ class AdvantageAirEntity(CoordinatorEntity):
self.async_change = instance["async_change"] self.async_change = instance["async_change"]
self.ac_key = ac_key self.ac_key = ac_key
self.zone_key = zone_key self.zone_key = zone_key
self._attr_device_info = {
"identifiers": {(DOMAIN, self.coordinator.data["system"]["rid"])},
"name": self.coordinator.data["system"]["name"],
"manufacturer": "Advantage Air",
"model": self.coordinator.data["system"]["sysType"],
"sw_version": self.coordinator.data["system"]["myAppRev"],
}
@property @property
def _ac(self): def _ac(self):
@ -22,14 +29,3 @@ class AdvantageAirEntity(CoordinatorEntity):
@property @property
def _zone(self): def _zone(self):
return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key] return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key]
@property
def device_info(self):
"""Return parent device information."""
return {
"identifiers": {(DOMAIN, self.coordinator.data["system"]["rid"])},
"name": self.coordinator.data["system"]["name"],
"manufacturer": "Advantage Air",
"model": self.coordinator.data["system"]["sysType"],
"sw_version": self.coordinator.data["system"]["myAppRev"],
}

View File

@ -3,8 +3,12 @@
"name": "Advantage Air", "name": "Advantage Air",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/advantage_air", "documentation": "https://www.home-assistant.io/integrations/advantage_air",
"codeowners": ["@Bre77"], "codeowners": [
"requirements": ["advantage_air==0.2.1"], "@Bre77"
],
"requirements": [
"advantage_air==0.2.5"
],
"quality_scale": "platinum", "quality_scale": "platinum",
"iot_class": "local_polling" "iot_class": "local_polling"
} }

View File

@ -1,8 +1,8 @@
"""Sensor platform for Advantage Air integration.""" """Sensor platform for Advantage Air integration."""
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
from homeassistant.const import PERCENTAGE from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN
@ -25,9 +25,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities.append(AdvantageAirTimeTo(instance, ac_key, "On")) entities.append(AdvantageAirTimeTo(instance, ac_key, "On"))
entities.append(AdvantageAirTimeTo(instance, ac_key, "Off")) entities.append(AdvantageAirTimeTo(instance, ac_key, "Off"))
for zone_key, zone in ac_device["zones"].items(): for zone_key, zone in ac_device["zones"].items():
# Only show damper sensors when zone is in temperature control # Only show damper and temp sensors when zone is in temperature control
if zone["type"] != 0: if zone["type"] != 0:
entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key))
entities.append(AdvantageAirZoneTemp(instance, ac_key, zone_key))
# Only show wireless signal strength sensors when using wireless sensors # Only show wireless signal strength sensors when using wireless sensors
if zone["rssi"] > 0: if zone["rssi"] > 0:
entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key)) entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key))
@ -50,17 +51,11 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity):
"""Initialize the Advantage Air timer control.""" """Initialize the Advantage Air timer control."""
super().__init__(instance, ac_key) super().__init__(instance, ac_key)
self.action = action self.action = action
self._time_key = f"countDownTo{self.action}" self._time_key = f"countDownTo{action}"
self._attr_name = f'{self._ac["name"]} Time To {action}'
@property self._attr_unique_id = (
def name(self): f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{action}'
"""Return the name.""" )
return f'{self._ac["name"]} Time To {self.action}'
@property
def unique_id(self):
"""Return a unique id."""
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{self.action}'
@property @property
def state(self): def state(self):
@ -84,16 +79,15 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air Zone Vent Sensor.""" """Representation of Advantage Air Zone Vent Sensor."""
_attr_unit_of_measurement = PERCENTAGE _attr_unit_of_measurement = PERCENTAGE
_attr_state_class = STATE_CLASS_MEASUREMENT
@property def __init__(self, instance, ac_key, zone_key):
def name(self): """Initialize an Advantage Air Zone Vent Sensor."""
"""Return the name.""" super().__init__(instance, ac_key, zone_key=zone_key)
return f'{self._zone["name"]} Vent' self._attr_name = f'{self._zone["name"]} Vent'
self._attr_unique_id = (
@property f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-vent'
def unique_id(self): )
"""Return a unique id."""
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-vent'
@property @property
def state(self): def state(self):
@ -114,16 +108,15 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air Zone wireless signal sensor.""" """Representation of Advantage Air Zone wireless signal sensor."""
_attr_unit_of_measurement = PERCENTAGE _attr_unit_of_measurement = PERCENTAGE
_attr_state_class = STATE_CLASS_MEASUREMENT
@property def __init__(self, instance, ac_key, zone_key):
def name(self): """Initialize an Advantage Air Zone wireless signal sensor."""
"""Return the name.""" super().__init__(instance, ac_key, zone_key=zone_key)
return f'{self._zone["name"]} Signal' self._attr_name = f'{self._zone["name"]} Signal'
self._attr_unique_id = (
@property f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-signal'
def unique_id(self): )
"""Return a unique id."""
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-signal'
@property @property
def state(self): def state(self):
@ -142,3 +135,23 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
if self._zone["rssi"] >= 20: if self._zone["rssi"] >= 20:
return "mdi:wifi-strength-1" return "mdi:wifi-strength-1"
return "mdi:wifi-strength-outline" return "mdi:wifi-strength-outline"
class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air Zone wireless signal sensor."""
_attr_unit_of_measurement = TEMP_CELSIUS
_attr_state_class = STATE_CLASS_MEASUREMENT
_attr_icon = "mdi:thermometer"
_attr_entity_registry_enabled_default = False
def __init__(self, instance, ac_key, zone_key):
"""Initialize an Advantage Air Zone Temp Sensor."""
super().__init__(instance, ac_key, zone_key)
self._attr_name = f'{self._zone["name"]} Temperature'
self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-temp'
@property
def state(self):
"""Return the current value of the measured temperature."""
return self._zone["measuredTemp"]

View File

@ -25,26 +25,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AdvantageAirFreshAir(AdvantageAirEntity, ToggleEntity): class AdvantageAirFreshAir(AdvantageAirEntity, ToggleEntity):
"""Representation of Advantage Air fresh air control.""" """Representation of Advantage Air fresh air control."""
@property _attr_icon = "mdi:air-filter"
def name(self):
"""Return the name."""
return f'{self._ac["name"]} Fresh Air'
@property def __init__(self, instance, ac_key):
def unique_id(self): """Initialize an Advantage Air fresh air control."""
"""Return a unique id.""" super().__init__(instance, ac_key)
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-freshair' self._attr_name = f'{self._ac["name"]} Fresh Air'
self._attr_unique_id = (
f'{self.coordinator.data["system"]["rid"]}-{ac_key}-freshair'
)
@property @property
def is_on(self): def is_on(self):
"""Return the fresh air status.""" """Return the fresh air status."""
return self._ac["freshAirStatus"] == ADVANTAGE_AIR_STATE_ON return self._ac["freshAirStatus"] == ADVANTAGE_AIR_STATE_ON
@property
def icon(self):
"""Return a representative icon of the fresh air switch."""
return "mdi:air-filter"
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn fresh air on.""" """Turn fresh air on."""
await self.async_change( await self.async_change(

View File

@ -9,10 +9,10 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"ip_address": "IP Adresse", "ip_address": "IP-Adresse",
"port": "Port" "port": "Port"
}, },
"description": "Anschluss an die API Ihres Advantage Air Wandtabletts.", "description": "Anschluss an die API deines Advantage Air Wandtabletts.",
"title": "Verbinden" "title": "Verbinden"
} }
} }

View File

@ -66,6 +66,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AbstractAemetSensor(CoordinatorEntity, SensorEntity): class AbstractAemetSensor(CoordinatorEntity, SensorEntity):
"""Abstract class for an AEMET OpenData sensor.""" """Abstract class for an AEMET OpenData sensor."""
_attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
def __init__( def __init__(
self, self,
name, name,
@ -80,33 +82,10 @@ class AbstractAemetSensor(CoordinatorEntity, SensorEntity):
self._unique_id = unique_id self._unique_id = unique_id
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._sensor_name = sensor_configuration[SENSOR_NAME] self._sensor_name = sensor_configuration[SENSOR_NAME]
self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) self._attr_name = f"{self._name} {self._sensor_name}"
self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS) self._attr_unique_id = self._unique_id
self._attr_device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS)
@property self._attr_unit_of_measurement = sensor_configuration.get(SENSOR_UNIT)
def name(self):
"""Return the name of the sensor."""
return f"{self._name} {self._sensor_name}"
@property
def unique_id(self):
"""Return a unique_id for this entity."""
return self._unique_id
@property
def device_class(self):
"""Return the device_class."""
return self._device_class
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
class AemetSensor(AbstractAemetSensor): class AemetSensor(AbstractAemetSensor):
@ -150,11 +129,9 @@ class AemetForecastSensor(AbstractAemetSensor):
) )
self._weather_coordinator = weather_coordinator self._weather_coordinator = weather_coordinator
self._forecast_mode = forecast_mode self._forecast_mode = forecast_mode
self._attr_entity_registry_enabled_default = (
@property self._forecast_mode == FORECAST_MODE_DAILY
def entity_registry_enabled_default(self) -> bool: )
"""Return if the entity should be enabled when first added to the entity registry."""
return self._forecast_mode == FORECAST_MODE_DAILY
@property @property
def state(self): def state(self):

View File

@ -0,0 +1,11 @@
{
"options": {
"step": {
"init": {
"data": {
"station_updates": "\u062c\u0645\u0639 \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0645\u0646 \u0645\u062d\u0637\u0627\u062a \u0627\u0644\u0637\u0642\u0633 AEMET"
}
}
}
}
}

View File

@ -15,7 +15,7 @@
"name": "Name der Integration" "name": "Name der Integration"
}, },
"description": "Richte die AEMET OpenData Integration ein. Um den API-Schl\u00fcssel zu generieren, besuche https://opendata.aemet.es/centrodedescargas/altaUsuario", "description": "Richte die AEMET OpenData Integration ein. Um den API-Schl\u00fcssel zu generieren, besuche https://opendata.aemet.es/centrodedescargas/altaUsuario",
"title": "[void]" "title": "AEMET OpenData"
} }
} }
}, },

View File

@ -18,5 +18,14 @@
"title": "AEMET OpenData" "title": "AEMET OpenData"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "Recueillir les donn\u00e9es des stations m\u00e9t\u00e9orologiques AEMET"
}
}
}
} }
} }

View File

@ -14,8 +14,18 @@
"longitude": "Hossz\u00fas\u00e1g", "longitude": "Hossz\u00fas\u00e1g",
"name": "Az integr\u00e1ci\u00f3 neve" "name": "Az integr\u00e1ci\u00f3 neve"
}, },
"description": "\u00c1ll\u00edtsa be az AEMET OpenData integr\u00e1ci\u00f3t. Az API-kulcs el\u0151\u00e1ll\u00edt\u00e1s\u00e1hoz keresse fel a https://opendata.aemet.es/centrodedescargas/altaUsuario webhelyet.",
"title": "AEMET OpenData" "title": "AEMET OpenData"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "Gy\u0171jts\u00f6n adatokat az AEMET meteorol\u00f3giai \u00e1llom\u00e1sokr\u00f3l"
}
}
}
} }
} }

View File

@ -18,5 +18,14 @@
"title": "AEMET OpenData" "title": "AEMET OpenData"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"station_updates": "Kumpulkan data dari stasiun cuaca AEMET"
}
}
}
} }
} }

View File

@ -39,6 +39,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AemetWeather(CoordinatorEntity, WeatherEntity): class AemetWeather(CoordinatorEntity, WeatherEntity):
"""Implementation of an AEMET OpenData sensor.""" """Implementation of an AEMET OpenData sensor."""
_attr_attribution = ATTRIBUTION
_attr_temperature_unit = TEMP_CELSIUS
def __init__( def __init__(
self, self,
name, name,
@ -48,25 +51,18 @@ class AemetWeather(CoordinatorEntity, WeatherEntity):
): ):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self._name = name
self._unique_id = unique_id
self._forecast_mode = forecast_mode self._forecast_mode = forecast_mode
self._attr_entity_registry_enabled_default = (
@property self._forecast_mode == FORECAST_MODE_DAILY
def attribution(self): )
"""Return the attribution.""" self._attr_name = name
return ATTRIBUTION self._attr_unique_id = unique_id
@property @property
def condition(self): def condition(self):
"""Return the current condition.""" """Return the current condition."""
return self.coordinator.data[ATTR_API_CONDITION] return self.coordinator.data[ATTR_API_CONDITION]
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return self._forecast_mode == FORECAST_MODE_DAILY
@property @property
def forecast(self): def forecast(self):
"""Return the forecast array.""" """Return the forecast array."""
@ -77,11 +73,6 @@ class AemetWeather(CoordinatorEntity, WeatherEntity):
"""Return the humidity.""" """Return the humidity."""
return self.coordinator.data[ATTR_API_HUMIDITY] return self.coordinator.data[ATTR_API_HUMIDITY]
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property @property
def pressure(self): def pressure(self):
"""Return the pressure.""" """Return the pressure."""
@ -92,16 +83,6 @@ class AemetWeather(CoordinatorEntity, WeatherEntity):
"""Return the temperature.""" """Return the temperature."""
return self.coordinator.data[ATTR_API_TEMPERATURE] return self.coordinator.data[ATTR_API_TEMPERATURE]
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def unique_id(self):
"""Return a unique_id for this entity."""
return self._unique_id
@property @property
def wind_bearing(self): def wind_bearing(self):
"""Return the temperature.""" """Return the temperature."""

View File

@ -109,38 +109,26 @@ async def async_setup_platform(
class AfterShipSensor(SensorEntity): class AfterShipSensor(SensorEntity):
"""Representation of a AfterShip sensor.""" """Representation of a AfterShip sensor."""
_attr_unit_of_measurement: str = "packages"
_attr_icon: str = ICON
def __init__(self, aftership: Tracking, name: str) -> None: def __init__(self, aftership: Tracking, name: str) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
self._attributes: dict[str, Any] = {} self._attributes: dict[str, Any] = {}
self._name: str = name
self._state: int | None = None self._state: int | None = None
self.aftership = aftership self.aftership = aftership
self._attr_name = name
@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property @property
def state(self) -> int | None: def state(self) -> int | None:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._state return self._state
@property
def unit_of_measurement(self) -> str:
"""Return the unit of measurement of this entity, if any."""
return "packages"
@property @property
def extra_state_attributes(self) -> dict[str, str]: def extra_state_attributes(self) -> dict[str, str]:
"""Return attributes for the sensor.""" """Return attributes for the sensor."""
return self._attributes return self._attributes
@property
def icon(self) -> str:
"""Icon to use in the frontend."""
return ICON
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Register callbacks.""" """Register callbacks."""
self.async_on_remove( self.async_on_remove(

View File

@ -35,90 +35,60 @@ async def async_setup_entry(
class AgentBaseStation(AlarmControlPanelEntity): class AgentBaseStation(AlarmControlPanelEntity):
"""Representation of an Agent DVR Alarm Control Panel.""" """Representation of an Agent DVR Alarm Control Panel."""
_attr_icon = ICON
_attr_supported_features = (
SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT
)
def __init__(self, client): def __init__(self, client):
"""Initialize the alarm control panel.""" """Initialize the alarm control panel."""
self._state = None
self._client = client self._client = client
self._unique_id = f"{client.unique}_CP" self._attr_name = f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}"
name = CONST_ALARM_CONTROL_PANEL_NAME self._attr_unique_id = f"{client.unique}_CP"
self._name = name = f"{client.name} {name}" self._attr_device_info = {
"identifiers": {(AGENT_DOMAIN, client.unique)},
@property
def icon(self):
"""Return icon."""
return ICON
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT
@property
def device_info(self):
"""Return the device info for adding the entity to the agent object."""
return {
"identifiers": {(AGENT_DOMAIN, self._client.unique)},
"manufacturer": "Agent", "manufacturer": "Agent",
"model": CONST_ALARM_CONTROL_PANEL_NAME, "model": CONST_ALARM_CONTROL_PANEL_NAME,
"sw_version": self._client.version, "sw_version": client.version,
} }
async def async_update(self): async def async_update(self):
"""Update the state of the device.""" """Update the state of the device."""
await self._client.update() await self._client.update()
self._attr_available = self._client.is_available
armed = self._client.is_armed armed = self._client.is_armed
if armed is None: if armed is None:
self._state = None self._attr_state = None
return return
if armed: if armed:
prof = (await self._client.get_active_profile()).lower() prof = (await self._client.get_active_profile()).lower()
self._state = STATE_ALARM_ARMED_AWAY self._attr_state = STATE_ALARM_ARMED_AWAY
if prof == CONF_HOME_MODE_NAME: if prof == CONF_HOME_MODE_NAME:
self._state = STATE_ALARM_ARMED_HOME self._attr_state = STATE_ALARM_ARMED_HOME
elif prof == CONF_NIGHT_MODE_NAME: elif prof == CONF_NIGHT_MODE_NAME:
self._state = STATE_ALARM_ARMED_NIGHT self._attr_state = STATE_ALARM_ARMED_NIGHT
else: else:
self._state = STATE_ALARM_DISARMED self._attr_state = STATE_ALARM_DISARMED
async def async_alarm_disarm(self, code=None): async def async_alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
await self._client.disarm() await self._client.disarm()
self._state = STATE_ALARM_DISARMED self._attr_state = STATE_ALARM_DISARMED
async def async_alarm_arm_away(self, code=None): async def async_alarm_arm_away(self, code=None):
"""Send arm away command. Uses custom mode.""" """Send arm away command. Uses custom mode."""
await self._client.arm() await self._client.arm()
await self._client.set_active_profile(CONF_AWAY_MODE_NAME) await self._client.set_active_profile(CONF_AWAY_MODE_NAME)
self._state = STATE_ALARM_ARMED_AWAY self._attr_state = STATE_ALARM_ARMED_AWAY
async def async_alarm_arm_home(self, code=None): async def async_alarm_arm_home(self, code=None):
"""Send arm home command. Uses custom mode.""" """Send arm home command. Uses custom mode."""
await self._client.arm() await self._client.arm()
await self._client.set_active_profile(CONF_HOME_MODE_NAME) await self._client.set_active_profile(CONF_HOME_MODE_NAME)
self._state = STATE_ALARM_ARMED_HOME self._attr_state = STATE_ALARM_ARMED_HOME
async def async_alarm_arm_night(self, code=None): async def async_alarm_arm_night(self, code=None):
"""Send arm night command. Uses custom mode.""" """Send arm night command. Uses custom mode."""
await self._client.arm() await self._client.arm()
await self._client.set_active_profile(CONF_NIGHT_MODE_NAME) await self._client.set_active_profile(CONF_NIGHT_MODE_NAME)
self._state = STATE_ALARM_ARMED_NIGHT self._attr_state = STATE_ALARM_ARMED_NIGHT
@property
def name(self):
"""Return the name of the base station."""
return self._name
@property
def available(self) -> bool:
"""Device available."""
return self._client.is_available
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id

View File

@ -67,31 +67,27 @@ async def async_setup_entry(
class AgentCamera(MjpegCamera): class AgentCamera(MjpegCamera):
"""Representation of an Agent Device Stream.""" """Representation of an Agent Device Stream."""
_attr_supported_features = SUPPORT_ON_OFF
def __init__(self, device): def __init__(self, device):
"""Initialize as a subclass of MjpegCamera.""" """Initialize as a subclass of MjpegCamera."""
self._servername = device.client.name
self.server_url = device.client._server_url
device_info = { device_info = {
CONF_NAME: device.name, CONF_NAME: device.name,
CONF_MJPEG_URL: f"{self.server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", CONF_MJPEG_URL: f"{device.client._server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}",
CONF_STILL_IMAGE_URL: f"{self.server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", CONF_STILL_IMAGE_URL: f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}",
} }
self.device = device self.device = device
self._removed = False self._removed = False
self._name = f"{self._servername} {device.name}" self._attr_name = f"{device.client.name} {device.name}"
self._unique_id = f"{device._client.unique}_{device.typeID}_{device.id}" self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}"
self._attr_should_poll = True
super().__init__(device_info) super().__init__(device_info)
self._attr_device_info = {
@property "identifiers": {(AGENT_DOMAIN, self.unique_id)},
def device_info(self): "name": self.name,
"""Return the device info for adding the entity to the agent object."""
return {
"identifiers": {(AGENT_DOMAIN, self._unique_id)},
"name": self._name,
"manufacturer": "Agent", "manufacturer": "Agent",
"model": "Camera", "model": "Camera",
"sw_version": self.device.client.version, "sw_version": device.client.version,
} }
async def async_update(self): async def async_update(self):
@ -99,18 +95,18 @@ class AgentCamera(MjpegCamera):
try: try:
await self.device.update() await self.device.update()
if self._removed: if self._removed:
_LOGGER.debug("%s reacquired", self._name) _LOGGER.debug("%s reacquired", self.name)
self._removed = False self._removed = False
except AgentError: except AgentError:
# server still available - camera error # server still available - camera error
if self.device.client.is_available and not self._removed: if self.device.client.is_available and not self._removed:
_LOGGER.error("%s lost", self._name) _LOGGER.error("%s lost", self.name)
self._removed = True self._removed = True
self._attr_available = self.device.client.is_available
@property self._attr_icon = "mdi:camcorder-off"
def extra_state_attributes(self): if self.is_on:
"""Return the Agent DVR camera state attributes.""" self._attr_icon = "mdi:camcorder"
return { self._attr_extra_state_attributes = {
ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_ATTRIBUTION: ATTRIBUTION,
"editable": False, "editable": False,
"enabled": self.is_on, "enabled": self.is_on,
@ -121,11 +117,6 @@ class AgentCamera(MjpegCamera):
"alerts_enabled": self.device.alerts_active, "alerts_enabled": self.device.alerts_active,
} }
@property
def should_poll(self) -> bool:
"""Update the state periodically."""
return True
@property @property
def is_recording(self) -> bool: def is_recording(self) -> bool:
"""Return whether the monitor is recording.""" """Return whether the monitor is recording."""
@ -141,43 +132,21 @@ class AgentCamera(MjpegCamera):
"""Return whether the monitor has alerted.""" """Return whether the monitor has alerted."""
return self.device.detected return self.device.detected
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.device.client.is_available
@property @property
def connected(self) -> bool: def connected(self) -> bool:
"""Return True if entity is connected.""" """Return True if entity is connected."""
return self.device.connected return self.device.connected
@property
def supported_features(self) -> int:
"""Return supported features."""
return SUPPORT_ON_OFF
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if on.""" """Return true if on."""
return self.device.online return self.device.online
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
if self.is_on:
return "mdi:camcorder"
return "mdi:camcorder-off"
@property @property
def motion_detection_enabled(self): def motion_detection_enabled(self):
"""Return the camera motion detection status.""" """Return the camera motion detection status."""
return self.device.detector_active return self.device.detector_active
@property
def unique_id(self) -> str:
"""Return a unique identifier for this agent object."""
return self._unique_id
async def async_enable_alerts(self): async def async_enable_alerts(self):
"""Enable alerts.""" """Enable alerts."""
await self.device.alerts_on() await self.device.alerts_on()

View File

@ -13,7 +13,7 @@
"host": "Host", "host": "Host",
"port": "Port" "port": "Port"
}, },
"title": "Richten Sie den Agent DVR ein" "title": "Richte den Agent DVR ein"
} }
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4"
}, },
"error": { "error": {
"already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea",

View File

@ -3,10 +3,8 @@ from __future__ import annotations
from typing import Final from typing import Final
from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ICON,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE, DEVICE_CLASS_PRESSURE,
@ -16,7 +14,7 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from .model import SensorDescription from .model import AirlySensorEntityDescription
ATTR_API_ADVICE: Final = "ADVICE" ATTR_API_ADVICE: Final = "ADVICE"
ATTR_API_CAQI: Final = "CAQI" ATTR_API_CAQI: Final = "CAQI"
@ -31,12 +29,9 @@ ATTR_API_TEMPERATURE: Final = "TEMPERATURE"
ATTR_ADVICE: Final = "advice" ATTR_ADVICE: Final = "advice"
ATTR_DESCRIPTION: Final = "description" ATTR_DESCRIPTION: Final = "description"
ATTR_LABEL: Final = "label"
ATTR_LEVEL: Final = "level" ATTR_LEVEL: Final = "level"
ATTR_LIMIT: Final = "limit" ATTR_LIMIT: Final = "limit"
ATTR_PERCENT: Final = "percent" ATTR_PERCENT: Final = "percent"
ATTR_UNIT: Final = "unit"
ATTR_VALUE: Final = "value"
SUFFIX_PERCENT: Final = "PERCENT" SUFFIX_PERCENT: Final = "PERCENT"
SUFFIX_LIMIT: Final = "LIMIT" SUFFIX_LIMIT: Final = "LIMIT"
@ -51,52 +46,54 @@ MAX_UPDATE_INTERVAL: Final = 90
MIN_UPDATE_INTERVAL: Final = 5 MIN_UPDATE_INTERVAL: Final = 5
NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet." NO_AIRLY_SENSORS: Final = "There are no Airly sensors in this area yet."
SENSOR_TYPES: dict[str, SensorDescription] = { SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = (
ATTR_API_CAQI: { AirlySensorEntityDescription(
ATTR_LABEL: ATTR_API_CAQI, key=ATTR_API_CAQI,
ATTR_UNIT: "CAQI", name=ATTR_API_CAQI,
ATTR_VALUE: round, unit_of_measurement="CAQI",
}, ),
ATTR_API_PM1: { AirlySensorEntityDescription(
ATTR_ICON: "mdi:blur", key=ATTR_API_PM1,
ATTR_LABEL: ATTR_API_PM1, icon="mdi:blur",
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, name=ATTR_API_PM1,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_VALUE: round, state_class=STATE_CLASS_MEASUREMENT,
}, ),
ATTR_API_PM25: { AirlySensorEntityDescription(
ATTR_ICON: "mdi:blur", key=ATTR_API_PM25,
ATTR_LABEL: "PM2.5", icon="mdi:blur",
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, name="PM2.5",
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_VALUE: round, state_class=STATE_CLASS_MEASUREMENT,
}, ),
ATTR_API_PM10: { AirlySensorEntityDescription(
ATTR_ICON: "mdi:blur", key=ATTR_API_PM10,
ATTR_LABEL: ATTR_API_PM10, icon="mdi:blur",
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, name=ATTR_API_PM10,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_VALUE: round, state_class=STATE_CLASS_MEASUREMENT,
}, ),
ATTR_API_HUMIDITY: { AirlySensorEntityDescription(
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, key=ATTR_API_HUMIDITY,
ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(), device_class=DEVICE_CLASS_HUMIDITY,
ATTR_UNIT: PERCENTAGE, name=ATTR_API_HUMIDITY.capitalize(),
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, unit_of_measurement=PERCENTAGE,
ATTR_VALUE: lambda value: round(value, 1), state_class=STATE_CLASS_MEASUREMENT,
}, value=lambda value: round(value, 1),
ATTR_API_PRESSURE: { ),
ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, AirlySensorEntityDescription(
ATTR_LABEL: ATTR_API_PRESSURE.capitalize(), key=ATTR_API_PRESSURE,
ATTR_UNIT: PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, name=ATTR_API_PRESSURE.capitalize(),
ATTR_VALUE: round, unit_of_measurement=PRESSURE_HPA,
}, state_class=STATE_CLASS_MEASUREMENT,
ATTR_API_TEMPERATURE: { ),
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, AirlySensorEntityDescription(
ATTR_LABEL: ATTR_API_TEMPERATURE.capitalize(), key=ATTR_API_TEMPERATURE,
ATTR_UNIT: TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE,
ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, name=ATTR_API_TEMPERATURE.capitalize(),
ATTR_VALUE: lambda value: round(value, 1), unit_of_measurement=TEMP_CELSIUS,
}, state_class=STATE_CLASS_MEASUREMENT,
} value=lambda value: round(value, 1),
),
)

View File

@ -1,15 +1,14 @@
"""Type definitions for Airly integration.""" """Type definitions for Airly integration."""
from __future__ import annotations from __future__ import annotations
from typing import Callable, TypedDict from dataclasses import dataclass
from typing import Callable
from homeassistant.components.sensor import SensorEntityDescription
class SensorDescription(TypedDict, total=False): @dataclass
"""Sensor description class.""" class AirlySensorEntityDescription(SensorEntityDescription):
"""Class describing Airly sensor entities."""
device_class: str | None value: Callable = round
icon: str | None
label: str
unit: str
state_class: str | None
value: Callable

View File

@ -3,16 +3,10 @@ from __future__ import annotations
from typing import Any, cast from typing import Any, cast
from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
ATTR_ICON,
CONF_NAME,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -27,12 +21,9 @@ from .const import (
ATTR_API_PM10, ATTR_API_PM10,
ATTR_API_PM25, ATTR_API_PM25,
ATTR_DESCRIPTION, ATTR_DESCRIPTION,
ATTR_LABEL,
ATTR_LEVEL, ATTR_LEVEL,
ATTR_LIMIT, ATTR_LIMIT,
ATTR_PERCENT, ATTR_PERCENT,
ATTR_UNIT,
ATTR_VALUE,
ATTRIBUTION, ATTRIBUTION,
DEFAULT_NAME, DEFAULT_NAME,
DOMAIN, DOMAIN,
@ -41,6 +32,7 @@ from .const import (
SUFFIX_LIMIT, SUFFIX_LIMIT,
SUFFIX_PERCENT, SUFFIX_PERCENT,
) )
from .model import AirlySensorEntityDescription
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@ -54,10 +46,10 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][entry.entry_id] coordinator = hass.data[DOMAIN][entry.entry_id]
sensors = [] sensors = []
for sensor in SENSOR_TYPES: for description in SENSOR_TYPES:
# When we use the nearest method, we are not sure which sensors are available # When we use the nearest method, we are not sure which sensors are available
if coordinator.data.get(sensor): if coordinator.data.get(description.key):
sensors.append(AirlySensor(coordinator, name, sensor)) sensors.append(AirlySensor(coordinator, name, description))
async_add_entities(sensors, False) async_add_entities(sensors, False)
@ -66,47 +58,54 @@ class AirlySensor(CoordinatorEntity, SensorEntity):
"""Define an Airly sensor.""" """Define an Airly sensor."""
coordinator: AirlyDataUpdateCoordinator coordinator: AirlyDataUpdateCoordinator
entity_description: AirlySensorEntityDescription
def __init__( def __init__(
self, coordinator: AirlyDataUpdateCoordinator, name: str, kind: str self,
coordinator: AirlyDataUpdateCoordinator,
name: str,
description: AirlySensorEntityDescription,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._description = description = SENSOR_TYPES[kind] self._attr_device_info = {
self._attr_device_class = description.get(ATTR_DEVICE_CLASS) "identifiers": {
self._attr_icon = description.get(ATTR_ICON) (DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}")
self._attr_name = f"{name} {description[ATTR_LABEL]}" },
self._attr_state_class = description.get(ATTR_STATE_CLASS) "name": DEFAULT_NAME,
"manufacturer": MANUFACTURER,
"entry_type": "service",
}
self._attr_name = f"{name} {description.name}"
self._attr_unique_id = ( self._attr_unique_id = (
f"{coordinator.latitude}-{coordinator.longitude}-{kind.lower()}" f"{coordinator.latitude}-{coordinator.longitude}-{description.key}".lower()
) )
self._attr_unit_of_measurement = description.get(ATTR_UNIT)
self._attrs: dict[str, Any] = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attrs: dict[str, Any] = {ATTR_ATTRIBUTION: ATTRIBUTION}
self.kind = kind self.entity_description = description
@property @property
def state(self) -> StateType: def state(self) -> StateType:
"""Return the state.""" """Return the state."""
state = self.coordinator.data[self.kind] state = self.coordinator.data[self.entity_description.key]
return cast(StateType, self._description[ATTR_VALUE](state)) return cast(StateType, self.entity_description.value(state))
@property @property
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes.""" """Return the state attributes."""
if self.kind == ATTR_API_CAQI: if self.entity_description.key == ATTR_API_CAQI:
self._attrs[ATTR_LEVEL] = self.coordinator.data[ATTR_API_CAQI_LEVEL] self._attrs[ATTR_LEVEL] = self.coordinator.data[ATTR_API_CAQI_LEVEL]
self._attrs[ATTR_ADVICE] = self.coordinator.data[ATTR_API_ADVICE] self._attrs[ATTR_ADVICE] = self.coordinator.data[ATTR_API_ADVICE]
self._attrs[ATTR_DESCRIPTION] = self.coordinator.data[ self._attrs[ATTR_DESCRIPTION] = self.coordinator.data[
ATTR_API_CAQI_DESCRIPTION ATTR_API_CAQI_DESCRIPTION
] ]
if self.kind == ATTR_API_PM25: if self.entity_description.key == ATTR_API_PM25:
self._attrs[ATTR_LIMIT] = self.coordinator.data[ self._attrs[ATTR_LIMIT] = self.coordinator.data[
f"{ATTR_API_PM25}_{SUFFIX_LIMIT}" f"{ATTR_API_PM25}_{SUFFIX_LIMIT}"
] ]
self._attrs[ATTR_PERCENT] = round( self._attrs[ATTR_PERCENT] = round(
self.coordinator.data[f"{ATTR_API_PM25}_{SUFFIX_PERCENT}"] self.coordinator.data[f"{ATTR_API_PM25}_{SUFFIX_PERCENT}"]
) )
if self.kind == ATTR_API_PM10: if self.entity_description.key == ATTR_API_PM10:
self._attrs[ATTR_LIMIT] = self.coordinator.data[ self._attrs[ATTR_LIMIT] = self.coordinator.data[
f"{ATTR_API_PM10}_{SUFFIX_LIMIT}" f"{ATTR_API_PM10}_{SUFFIX_LIMIT}"
] ]
@ -114,18 +113,3 @@ class AirlySensor(CoordinatorEntity, SensorEntity):
self.coordinator.data[f"{ATTR_API_PM10}_{SUFFIX_PERCENT}"] self.coordinator.data[f"{ATTR_API_PM10}_{SUFFIX_PERCENT}"]
) )
return self._attrs return self._attrs
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return {
"identifiers": {
(
DOMAIN,
f"{self.coordinator.latitude}-{self.coordinator.longitude}",
)
},
"name": DEFAULT_NAME,
"manufacturer": MANUFACTURER,
"entry_type": "service",
}

View File

@ -22,6 +22,7 @@
}, },
"system_health": { "system_health": {
"info": { "info": {
"can_reach_server": "\u00c9rje el az Airly szervert",
"requests_per_day": "Enged\u00e9lyezett k\u00e9r\u00e9sek naponta", "requests_per_day": "Enged\u00e9lyezett k\u00e9r\u00e9sek naponta",
"requests_remaining": "Fennmarad\u00f3 enged\u00e9lyezett k\u00e9r\u00e9sek" "requests_remaining": "Fennmarad\u00f3 enged\u00e9lyezett k\u00e9r\u00e9sek"
} }

View File

@ -67,16 +67,13 @@ class AirNowSensor(CoordinatorEntity, SensorEntity):
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self.kind = kind self.kind = kind
self._device_class = None
self._state = None self._state = None
self._icon = None
self._unit_of_measurement = None
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
self._attr_name = f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}"
@property self._attr_icon = SENSOR_TYPES[self.kind][ATTR_ICON]
def name(self): self._attr_device_class = SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
"""Return the name.""" self._attr_unit_of_measurement = SENSOR_TYPES[self.kind][ATTR_UNIT]
return f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}" self._attr_unique_id = f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}"
@property @property
def state(self): def state(self):
@ -96,24 +93,3 @@ class AirNowSensor(CoordinatorEntity, SensorEntity):
] ]
return self._attrs return self._attrs
@property
def icon(self):
"""Return the icon."""
self._icon = SENSOR_TYPES[self.kind][ATTR_ICON]
return self._icon
@property
def device_class(self):
"""Return the device_class."""
return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
@property
def unique_id(self):
"""Return a unique_id for this entity."""
return f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}"
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return SENSOR_TYPES[self.kind][ATTR_UNIT]

View File

@ -1,5 +1,4 @@
{ {
"title": "AirNow",
"config": { "config": {
"step": { "step": {
"user": { "user": {

View File

@ -17,7 +17,7 @@
"longitude": "L\u00e4ngengrad", "longitude": "L\u00e4ngengrad",
"radius": "Stationsradius (Meilen; optional)" "radius": "Stationsradius (Meilen; optional)"
}, },
"description": "Richten Sie die AirNow-Luftqualit\u00e4tsintegration ein. Um den API-Schl\u00fcssel zu generieren, besuchen Sie https://docs.airnowapi.org/account/request/.", "description": "Richte die AirNow-Luftqualit\u00e4tsintegration ein. Um den API-Schl\u00fcssel zu generieren, besuche https://docs.airnowapi.org/account/request/.",
"title": "AirNow" "title": "AirNow"
} }
} }

View File

@ -14,8 +14,10 @@
"data": { "data": {
"api_key": "API kulcs", "api_key": "API kulcs",
"latitude": "Sz\u00e9less\u00e9g", "latitude": "Sz\u00e9less\u00e9g",
"longitude": "Hossz\u00fas\u00e1g" "longitude": "Hossz\u00fas\u00e1g",
"radius": "\u00c1llom\u00e1s sugara (m\u00e9rf\u00f6ld; opcion\u00e1lis)"
}, },
"description": "\u00c1ll\u00edtsa be az AirNow leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3t. Az API-kulcs el\u0151\u00e1ll\u00edt\u00e1s\u00e1hoz keresse fel a https://docs.airnowapi.org/account/request/ oldalt.",
"title": "AirNow" "title": "AirNow"
} }
} }

View File

@ -1,6 +1,10 @@
"""The airvisual component.""" """The airvisual component."""
from __future__ import annotations
from collections.abc import Mapping
from datetime import timedelta from datetime import timedelta
from math import ceil from math import ceil
from typing import Any
from pyairvisual import CloudAPI, NodeSamba from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import ( from pyairvisual.errors import (
@ -10,6 +14,7 @@ from pyairvisual.errors import (
NodeProError, NodeProError,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
CONF_API_KEY, CONF_API_KEY,
@ -20,9 +25,13 @@ from homeassistant.const import (
CONF_SHOW_ON_MAP, CONF_SHOW_ON_MAP,
CONF_STATE, CONF_STATE,
) )
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import (
aiohttp_client,
config_validation as cv,
entity_registry,
)
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,
@ -42,7 +51,7 @@ from .const import (
LOGGER, LOGGER,
) )
PLATFORMS = ["air_quality", "sensor"] PLATFORMS = ["sensor"]
DATA_LISTENER = "listener" DATA_LISTENER = "listener"
@ -53,11 +62,8 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN)
@callback @callback
def async_get_geography_id(geography_dict): def async_get_geography_id(geography_dict: Mapping[str, Any]) -> str:
"""Generate a unique ID from a geography dict.""" """Generate a unique ID from a geography dict."""
if not geography_dict:
return
if CONF_CITY in geography_dict: if CONF_CITY in geography_dict:
return ", ".join( return ", ".join(
( (
@ -72,7 +78,9 @@ def async_get_geography_id(geography_dict):
@callback @callback
def async_get_cloud_api_update_interval(hass, api_key, num_consumers): def async_get_cloud_api_update_interval(
hass: HomeAssistant, api_key: str, num_consumers: int
) -> timedelta:
"""Get a leveled scan interval for a particular cloud API key. """Get a leveled scan interval for a particular cloud API key.
This will shift based on the number of active consumers, thus keeping the user This will shift based on the number of active consumers, thus keeping the user
@ -93,18 +101,22 @@ def async_get_cloud_api_update_interval(hass, api_key, num_consumers):
@callback @callback
def async_get_cloud_coordinators_by_api_key(hass, api_key): def async_get_cloud_coordinators_by_api_key(
hass: HomeAssistant, api_key: str
) -> list[DataUpdateCoordinator]:
"""Get all DataUpdateCoordinator objects related to a particular API key.""" """Get all DataUpdateCoordinator objects related to a particular API key."""
coordinators = [] coordinators = []
for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items(): for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items():
config_entry = hass.config_entries.async_get_entry(entry_id) config_entry = hass.config_entries.async_get_entry(entry_id)
if config_entry.data.get(CONF_API_KEY) == api_key: if config_entry and config_entry.data.get(CONF_API_KEY) == api_key:
coordinators.append(coordinator) coordinators.append(coordinator)
return coordinators return coordinators
@callback @callback
def async_sync_geo_coordinator_update_intervals(hass, api_key): def async_sync_geo_coordinator_update_intervals(
hass: HomeAssistant, api_key: str
) -> None:
"""Sync the update interval for geography-based data coordinators (by API key).""" """Sync the update interval for geography-based data coordinators (by API key)."""
coordinators = async_get_cloud_coordinators_by_api_key(hass, api_key) coordinators = async_get_cloud_coordinators_by_api_key(hass, api_key)
@ -124,14 +136,10 @@ def async_sync_geo_coordinator_update_intervals(hass, api_key):
coordinator.update_interval = update_interval coordinator.update_interval = update_interval
async def async_setup(hass, config):
"""Set up the AirVisual component."""
hass.data[DOMAIN] = {DATA_COORDINATOR: {}, DATA_LISTENER: {}}
return True
@callback @callback
def _standardize_geography_config_entry(hass, config_entry): def _standardize_geography_config_entry(
hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
"""Ensure that geography config entries have appropriate properties.""" """Ensure that geography config entries have appropriate properties."""
entry_updates = {} entry_updates = {}
@ -164,9 +172,11 @@ def _standardize_geography_config_entry(hass, config_entry):
@callback @callback
def _standardize_node_pro_config_entry(hass, config_entry): def _standardize_node_pro_config_entry(
hass: HomeAssistant, config_entry: ConfigEntry
) -> None:
"""Ensure that Node/Pro config entries have appropriate properties.""" """Ensure that Node/Pro config entries have appropriate properties."""
entry_updates = {} entry_updates: dict[str, Any] = {}
if CONF_INTEGRATION_TYPE not in config_entry.data: if CONF_INTEGRATION_TYPE not in config_entry.data:
# If the config entry data doesn't contain the integration type, add it: # If the config entry data doesn't contain the integration type, add it:
@ -181,15 +191,17 @@ def _standardize_node_pro_config_entry(hass, config_entry):
hass.config_entries.async_update_entry(config_entry, **entry_updates) hass.config_entries.async_update_entry(config_entry, **entry_updates)
async def async_setup_entry(hass, config_entry): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up AirVisual as config entry.""" """Set up AirVisual as config entry."""
hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_LISTENER: {}})
if CONF_API_KEY in config_entry.data: if CONF_API_KEY in config_entry.data:
_standardize_geography_config_entry(hass, config_entry) _standardize_geography_config_entry(hass, config_entry)
websession = aiohttp_client.async_get_clientsession(hass) websession = aiohttp_client.async_get_clientsession(hass)
cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession) cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession)
async def async_update_data(): async def async_update_data() -> dict[str, Any]:
"""Get new data from the API.""" """Get new data from the API."""
if CONF_CITY in config_entry.data: if CONF_CITY in config_entry.data:
api_coro = cloud_api.air_quality.city( api_coro = cloud_api.air_quality.city(
@ -227,9 +239,22 @@ async def async_setup_entry(hass, config_entry):
config_entry.entry_id config_entry.entry_id
] = config_entry.add_update_listener(async_reload_entry) ] = config_entry.add_update_listener(async_reload_entry)
else: else:
# Remove outdated air_quality entities from the entity registry if they exist:
ent_reg = entity_registry.async_get(hass)
for entity_entry in [
e
for e in ent_reg.entities.values()
if e.config_entry_id == config_entry.entry_id
and e.entity_id.startswith("air_quality")
]:
LOGGER.debug(
'Removing deprecated air_quality entity: "%s"', entity_entry.entity_id
)
ent_reg.async_remove(entity_entry.entity_id)
_standardize_node_pro_config_entry(hass, config_entry) _standardize_node_pro_config_entry(hass, config_entry)
async def async_update_data(): async def async_update_data() -> dict[str, Any]:
"""Get new data from the API.""" """Get new data from the API."""
try: try:
async with NodeSamba( async with NodeSamba(
@ -262,7 +287,7 @@ async def async_setup_entry(hass, config_entry):
return True return True
async def async_migrate_entry(hass, config_entry): async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate an old config entry.""" """Migrate an old config entry."""
version = config_entry.version version = config_entry.version
@ -304,7 +329,7 @@ async def async_migrate_entry(hass, config_entry):
return True return True
async def async_unload_entry(hass, config_entry): async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload an AirVisual config entry.""" """Unload an AirVisual config entry."""
unload_ok = await hass.config_entries.async_unload_platforms( unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS config_entry, PLATFORMS
@ -325,7 +350,7 @@ async def async_unload_entry(hass, config_entry):
return unload_ok return unload_ok
async def async_reload_entry(hass, config_entry): async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Handle an options update.""" """Handle an options update."""
await hass.config_entries.async_reload(config_entry.entry_id) await hass.config_entries.async_reload(config_entry.entry_id)
@ -333,21 +358,17 @@ async def async_reload_entry(hass, config_entry):
class AirVisualEntity(CoordinatorEntity): class AirVisualEntity(CoordinatorEntity):
"""Define a generic AirVisual entity.""" """Define a generic AirVisual entity."""
def __init__(self, coordinator): def __init__(self, coordinator: DataUpdateCoordinator) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
@property self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
def extra_state_attributes(self):
"""Return the device state attributes."""
return self._attrs
async def async_added_to_hass(self): async def async_added_to_hass(self) -> None:
"""Register callbacks.""" """Register callbacks."""
@callback @callback
def update(): def update() -> None:
"""Update the state.""" """Update the state."""
self.update_from_latest_data() self.update_from_latest_data()
self.async_write_ha_state() self.async_write_ha_state()
@ -357,6 +378,6 @@ class AirVisualEntity(CoordinatorEntity):
self.update_from_latest_data() self.update_from_latest_data()
@callback @callback
def update_from_latest_data(self): def update_from_latest_data(self) -> None:
"""Update the entity from the latest data.""" """Update the entity from the latest data."""
raise NotImplementedError raise NotImplementedError

View File

@ -1,108 +0,0 @@
"""Support for AirVisual Node/Pro units."""
from homeassistant.components.air_quality import AirQualityEntity
from homeassistant.core import callback
from . import AirVisualEntity
from .const import (
CONF_INTEGRATION_TYPE,
DATA_COORDINATOR,
DOMAIN,
INTEGRATION_TYPE_NODE_PRO,
)
ATTR_HUMIDITY = "humidity"
ATTR_SENSOR_LIFE = "{0}_sensor_life"
ATTR_VOC = "voc"
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up AirVisual air quality entities based on a config entry."""
# Geography-based AirVisual integrations don't utilize this platform:
if config_entry.data[CONF_INTEGRATION_TYPE] != INTEGRATION_TYPE_NODE_PRO:
return
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
async_add_entities([AirVisualNodeProSensor(coordinator)], True)
class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity):
"""Define a sensor for a AirVisual Node/Pro."""
def __init__(self, airvisual):
"""Initialize."""
super().__init__(airvisual)
self._attr_icon = "mdi:chemical-weapon"
@property
def air_quality_index(self):
"""Return the Air Quality Index (AQI)."""
if self.coordinator.data["settings"]["is_aqi_usa"]:
return self.coordinator.data["measurements"]["aqi_us"]
return self.coordinator.data["measurements"]["aqi_cn"]
@property
def available(self):
"""Return True if entity is available."""
return bool(self.coordinator.data)
@property
def carbon_dioxide(self):
"""Return the CO2 (carbon dioxide) level."""
return self.coordinator.data["measurements"].get("co2")
@property
def device_info(self):
"""Return device registry information for this entity."""
return {
"identifiers": {(DOMAIN, self.coordinator.data["serial_number"])},
"name": self.coordinator.data["settings"]["node_name"],
"manufacturer": "AirVisual",
"model": f'{self.coordinator.data["status"]["model"]}',
"sw_version": (
f'Version {self.coordinator.data["status"]["system_version"]}'
f'{self.coordinator.data["status"]["app_version"]}'
),
}
@property
def name(self):
"""Return the name."""
node_name = self.coordinator.data["settings"]["node_name"]
return f"{node_name} Node/Pro: Air Quality"
@property
def particulate_matter_2_5(self):
"""Return the particulate matter 2.5 level."""
return self.coordinator.data["measurements"].get("pm2_5")
@property
def particulate_matter_10(self):
"""Return the particulate matter 10 level."""
return self.coordinator.data["measurements"].get("pm1_0")
@property
def particulate_matter_0_1(self):
"""Return the particulate matter 0.1 level."""
return self.coordinator.data["measurements"].get("pm0_1")
@property
def unique_id(self):
"""Return a unique, Home Assistant friendly identifier for this entity."""
return self.coordinator.data["serial_number"]
@callback
def update_from_latest_data(self):
"""Update the entity from the latest data."""
self._attrs.update(
{
ATTR_VOC: self.coordinator.data["measurements"].get("voc"),
**{
ATTR_SENSOR_LIFE.format(pollutant): lifespan
for pollutant, lifespan in self.coordinator.data["status"][
"sensor_life"
].items()
},
}
)

View File

@ -1,4 +1,6 @@
"""Define a config flow manager for AirVisual.""" """Define a config flow manager for AirVisual."""
from __future__ import annotations
import asyncio import asyncio
from pyairvisual import CloudAPI, NodeSamba from pyairvisual import CloudAPI, NodeSamba
@ -11,6 +13,7 @@ from pyairvisual.errors import (
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry, OptionsFlow
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_IP_ADDRESS, CONF_IP_ADDRESS,
@ -21,6 +24,7 @@ from homeassistant.const import (
CONF_STATE, CONF_STATE,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
from . import async_get_geography_id from . import async_get_geography_id
@ -64,13 +68,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 2 VERSION = 2
def __init__(self): def __init__(self) -> None:
"""Initialize the config flow.""" """Initialize the config flow."""
self._entry_data_for_reauth = None self._entry_data_for_reauth: dict[str, str] = {}
self._geo_id = None self._geo_id: str | None = None
@property @property
def geography_coords_schema(self): def geography_coords_schema(self) -> vol.Schema:
"""Return the data schema for the cloud API.""" """Return the data schema for the cloud API."""
return API_KEY_DATA_SCHEMA.extend( return API_KEY_DATA_SCHEMA.extend(
{ {
@ -83,7 +87,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
} }
) )
async def _async_finish_geography(self, user_input, integration_type): async def _async_finish_geography(
self, user_input: dict[str, str], integration_type: str
) -> FlowResult:
"""Validate a Cloud API key.""" """Validate a Cloud API key."""
websession = aiohttp_client.async_get_clientsession(self.hass) websession = aiohttp_client.async_get_clientsession(self.hass)
cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession) cloud_api = CloudAPI(user_input[CONF_API_KEY], session=websession)
@ -142,25 +148,29 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
data={**user_input, CONF_INTEGRATION_TYPE: integration_type}, data={**user_input, CONF_INTEGRATION_TYPE: integration_type},
) )
async def _async_init_geography(self, user_input, integration_type): async def _async_init_geography(
self, user_input: dict[str, str], integration_type: str
) -> FlowResult:
"""Handle the initialization of the integration via the cloud API.""" """Handle the initialization of the integration via the cloud API."""
self._geo_id = async_get_geography_id(user_input) self._geo_id = async_get_geography_id(user_input)
await self._async_set_unique_id(self._geo_id) await self._async_set_unique_id(self._geo_id)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return await self._async_finish_geography(user_input, integration_type) return await self._async_finish_geography(user_input, integration_type)
async def _async_set_unique_id(self, unique_id): async def _async_set_unique_id(self, unique_id: str) -> None:
"""Set the unique ID of the config flow and abort if it already exists.""" """Set the unique ID of the config flow and abort if it already exists."""
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Define the config flow to handle options.""" """Define the config flow to handle options."""
return AirVisualOptionsFlowHandler(config_entry) return AirVisualOptionsFlowHandler(config_entry)
async def async_step_geography_by_coords(self, user_input=None): async def async_step_geography_by_coords(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle the initialization of the cloud API based on latitude/longitude.""" """Handle the initialization of the cloud API based on latitude/longitude."""
if not user_input: if not user_input:
return self.async_show_form( return self.async_show_form(
@ -171,7 +181,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
user_input, INTEGRATION_TYPE_GEOGRAPHY_COORDS user_input, INTEGRATION_TYPE_GEOGRAPHY_COORDS
) )
async def async_step_geography_by_name(self, user_input=None): async def async_step_geography_by_name(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle the initialization of the cloud API based on city/state/country.""" """Handle the initialization of the cloud API based on city/state/country."""
if not user_input: if not user_input:
return self.async_show_form( return self.async_show_form(
@ -182,7 +194,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME user_input, INTEGRATION_TYPE_GEOGRAPHY_NAME
) )
async def async_step_node_pro(self, user_input=None): async def async_step_node_pro(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle the initialization of the integration with a Node/Pro.""" """Handle the initialization of the integration with a Node/Pro."""
if not user_input: if not user_input:
return self.async_show_form(step_id="node_pro", data_schema=NODE_PRO_SCHEMA) return self.async_show_form(step_id="node_pro", data_schema=NODE_PRO_SCHEMA)
@ -208,13 +222,15 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO}, data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO},
) )
async def async_step_reauth(self, data): async def async_step_reauth(self, data: dict[str, str]) -> FlowResult:
"""Handle configuration by re-auth.""" """Handle configuration by re-auth."""
self._entry_data_for_reauth = data self._entry_data_for_reauth = data
self._geo_id = async_get_geography_id(data) self._geo_id = async_get_geography_id(data)
return await self.async_step_reauth_confirm() return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None): async def async_step_reauth_confirm(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle re-auth completion.""" """Handle re-auth completion."""
if not user_input: if not user_input:
return self.async_show_form( return self.async_show_form(
@ -227,7 +243,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
conf, self._entry_data_for_reauth[CONF_INTEGRATION_TYPE] conf, self._entry_data_for_reauth[CONF_INTEGRATION_TYPE]
) )
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle the start of the config flow.""" """Handle the start of the config flow."""
if not user_input: if not user_input:
return self.async_show_form( return self.async_show_form(
@ -244,11 +262,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class AirVisualOptionsFlowHandler(config_entries.OptionsFlow): class AirVisualOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle an AirVisual options flow.""" """Handle an AirVisual options flow."""
def __init__(self, config_entry): def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize.""" """Initialize."""
self.config_entry = config_entry self.config_entry = config_entry
async def async_step_init(self, user_input=None): async def async_step_init(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Manage the options.""" """Manage the options."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)

View File

@ -3,7 +3,7 @@
"name": "AirVisual", "name": "AirVisual",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airvisual", "documentation": "https://www.home-assistant.io/integrations/airvisual",
"requirements": ["pyairvisual==5.0.8"], "requirements": ["pyairvisual==5.0.9"],
"codeowners": ["@bachya"], "codeowners": ["@bachya"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View File

@ -1,5 +1,8 @@
"""Support for AirVisual air quality sensors.""" """Support for AirVisual air quality sensors."""
from __future__ import annotations
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_LATITUDE, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_LONGITUDE,
@ -12,12 +15,16 @@ from homeassistant.const import (
CONF_SHOW_ON_MAP, CONF_SHOW_ON_MAP,
CONF_STATE, CONF_STATE,
DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY,
DEVICE_CLASS_CO2,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
PERCENTAGE, PERCENTAGE,
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import AirVisualEntity from . import AirVisualEntity
from .const import ( from .const import (
@ -36,12 +43,21 @@ ATTR_POLLUTANT_SYMBOL = "pollutant_symbol"
ATTR_POLLUTANT_UNIT = "pollutant_unit" ATTR_POLLUTANT_UNIT = "pollutant_unit"
ATTR_REGION = "region" ATTR_REGION = "region"
SENSOR_KIND_LEVEL = "air_pollution_level" DEVICE_CLASS_POLLUTANT_LABEL = "airvisual__pollutant_label"
DEVICE_CLASS_POLLUTANT_LEVEL = "airvisual__pollutant_level"
SENSOR_KIND_AQI = "air_quality_index" SENSOR_KIND_AQI = "air_quality_index"
SENSOR_KIND_POLLUTANT = "main_pollutant"
SENSOR_KIND_BATTERY_LEVEL = "battery_level" SENSOR_KIND_BATTERY_LEVEL = "battery_level"
SENSOR_KIND_CO2 = "carbon_dioxide"
SENSOR_KIND_HUMIDITY = "humidity" SENSOR_KIND_HUMIDITY = "humidity"
SENSOR_KIND_LEVEL = "air_pollution_level"
SENSOR_KIND_PM_0_1 = "particulate_matter_0_1"
SENSOR_KIND_PM_1_0 = "particulate_matter_1_0"
SENSOR_KIND_PM_2_5 = "particulate_matter_2_5"
SENSOR_KIND_POLLUTANT = "main_pollutant"
SENSOR_KIND_SENSOR_LIFE = "sensor_life"
SENSOR_KIND_TEMPERATURE = "temperature" SENSOR_KIND_TEMPERATURE = "temperature"
SENSOR_KIND_VOC = "voc"
GEOGRAPHY_SENSORS = [ GEOGRAPHY_SENSORS = [
(SENSOR_KIND_LEVEL, "Air Pollution Level", "mdi:gauge", None), (SENSOR_KIND_LEVEL, "Air Pollution Level", "mdi:gauge", None),
@ -51,27 +67,74 @@ GEOGRAPHY_SENSORS = [
GEOGRAPHY_SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."} GEOGRAPHY_SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."}
NODE_PRO_SENSORS = [ NODE_PRO_SENSORS = [
(SENSOR_KIND_BATTERY_LEVEL, "Battery", DEVICE_CLASS_BATTERY, PERCENTAGE), (SENSOR_KIND_AQI, "Air Quality Index", None, "mdi:chart-line", "AQI"),
(SENSOR_KIND_HUMIDITY, "Humidity", DEVICE_CLASS_HUMIDITY, PERCENTAGE), (SENSOR_KIND_BATTERY_LEVEL, "Battery", DEVICE_CLASS_BATTERY, None, PERCENTAGE),
(SENSOR_KIND_TEMPERATURE, "Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS), (
SENSOR_KIND_CO2,
"C02",
DEVICE_CLASS_CO2,
None,
CONCENTRATION_PARTS_PER_MILLION,
),
(SENSOR_KIND_HUMIDITY, "Humidity", DEVICE_CLASS_HUMIDITY, None, PERCENTAGE),
(
SENSOR_KIND_PM_0_1,
"PM 0.1",
None,
"mdi:sprinkler",
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
(
SENSOR_KIND_PM_1_0,
"PM 1.0",
None,
"mdi:sprinkler",
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
(
SENSOR_KIND_PM_2_5,
"PM 2.5",
None,
"mdi:sprinkler",
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
(
SENSOR_KIND_TEMPERATURE,
"Temperature",
DEVICE_CLASS_TEMPERATURE,
None,
TEMP_CELSIUS,
),
(
SENSOR_KIND_VOC,
"VOC",
None,
"mdi:sprinkler",
CONCENTRATION_PARTS_PER_MILLION,
),
] ]
POLLUTANT_LABELS = { STATE_POLLUTANT_LABEL_CO = "co"
"co": "Carbon Monoxide", STATE_POLLUTANT_LABEL_N2 = "n2"
"n2": "Nitrogen Dioxide", STATE_POLLUTANT_LABEL_O3 = "o3"
"o3": "Ozone", STATE_POLLUTANT_LABEL_P1 = "p1"
"p1": "PM10", STATE_POLLUTANT_LABEL_P2 = "p2"
"p2": "PM2.5", STATE_POLLUTANT_LABEL_S2 = "s2"
"s2": "Sulfur Dioxide",
} STATE_POLLUTANT_LEVEL_GOOD = "good"
STATE_POLLUTANT_LEVEL_MODERATE = "moderate"
STATE_POLLUTANT_LEVEL_UNHEALTHY_SENSITIVE = "unhealthy_sensitive"
STATE_POLLUTANT_LEVEL_UNHEALTHY = "unhealthy"
STATE_POLLUTANT_LEVEL_VERY_UNHEALTHY = "very_unhealthy"
STATE_POLLUTANT_LEVEL_HAZARDOUS = "hazardous"
POLLUTANT_LEVELS = { POLLUTANT_LEVELS = {
(0, 50): ("Good", "mdi:emoticon-excited"), (0, 50): (STATE_POLLUTANT_LEVEL_GOOD, "mdi:emoticon-excited"),
(51, 100): ("Moderate", "mdi:emoticon-happy"), (51, 100): (STATE_POLLUTANT_LEVEL_MODERATE, "mdi:emoticon-happy"),
(101, 150): ("Unhealthy for sensitive groups", "mdi:emoticon-neutral"), (101, 150): (STATE_POLLUTANT_LEVEL_UNHEALTHY_SENSITIVE, "mdi:emoticon-neutral"),
(151, 200): ("Unhealthy", "mdi:emoticon-sad"), (151, 200): (STATE_POLLUTANT_LEVEL_UNHEALTHY, "mdi:emoticon-sad"),
(201, 300): ("Very unhealthy", "mdi:emoticon-dead"), (201, 300): (STATE_POLLUTANT_LEVEL_VERY_UNHEALTHY, "mdi:emoticon-dead"),
(301, 1000): ("Hazardous", "mdi:biohazard"), (301, 1000): (STATE_POLLUTANT_LEVEL_HAZARDOUS, "mdi:biohazard"),
} }
POLLUTANT_UNITS = { POLLUTANT_UNITS = {
@ -84,10 +147,15 @@ POLLUTANT_UNITS = {
} }
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up AirVisual sensors based on a config entry.""" """Set up AirVisual sensors based on a config entry."""
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
sensors: list[AirVisualGeographySensor | AirVisualNodeProSensor]
if config_entry.data[CONF_INTEGRATION_TYPE] in [ if config_entry.data[CONF_INTEGRATION_TYPE] in [
INTEGRATION_TYPE_GEOGRAPHY_COORDS, INTEGRATION_TYPE_GEOGRAPHY_COORDS,
INTEGRATION_TYPE_GEOGRAPHY_NAME, INTEGRATION_TYPE_GEOGRAPHY_NAME,
@ -107,8 +175,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
] ]
else: else:
sensors = [ sensors = [
AirVisualNodeProSensor(coordinator, kind, name, device_class, unit) AirVisualNodeProSensor(coordinator, kind, name, device_class, icon, unit)
for kind, name, device_class, unit in NODE_PRO_SENSORS for kind, name, device_class, icon, unit in NODE_PRO_SENSORS
] ]
async_add_entities(sensors, True) async_add_entities(sensors, True)
@ -117,53 +185,45 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AirVisualGeographySensor(AirVisualEntity, SensorEntity): class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
"""Define an AirVisual sensor related to geography data via the Cloud API.""" """Define an AirVisual sensor related to geography data via the Cloud API."""
def __init__(self, coordinator, config_entry, kind, name, icon, unit, locale): def __init__(
self,
coordinator: DataUpdateCoordinator,
config_entry: ConfigEntry,
kind: str,
name: str,
icon: str,
unit: str | None,
locale: str,
) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._attrs.update( if kind == SENSOR_KIND_LEVEL:
self._attr_device_class = DEVICE_CLASS_POLLUTANT_LEVEL
elif kind == SENSOR_KIND_POLLUTANT:
self._attr_device_class = DEVICE_CLASS_POLLUTANT_LABEL
self._attr_extra_state_attributes.update(
{ {
ATTR_CITY: config_entry.data.get(CONF_CITY), ATTR_CITY: config_entry.data.get(CONF_CITY),
ATTR_STATE: config_entry.data.get(CONF_STATE), ATTR_STATE: config_entry.data.get(CONF_STATE),
ATTR_COUNTRY: config_entry.data.get(CONF_COUNTRY), ATTR_COUNTRY: config_entry.data.get(CONF_COUNTRY),
} }
) )
self._attr_icon = icon
self._attr_name = f"{GEOGRAPHY_SENSOR_LOCALES[locale]} {name}"
self._attr_unique_id = f"{config_entry.unique_id}_{locale}_{kind}"
self._attr_unit_of_measurement = unit
self._config_entry = config_entry self._config_entry = config_entry
self._kind = kind self._kind = kind
self._locale = locale self._locale = locale
self._name = name
self._state = None
self._attr_icon = icon
self._attr_unit_of_measurement = unit
@property @property
def available(self): def available(self) -> bool:
"""Return True if entity is available.""" """Return if entity is available."""
try: return super().available and self.coordinator.data["current"]["pollution"]
return self.coordinator.last_update_success and bool(
self.coordinator.data["current"]["pollution"]
)
except KeyError:
return False
@property
def name(self):
"""Return the name."""
return f"{GEOGRAPHY_SENSOR_LOCALES[self._locale]} {self._name}"
@property
def state(self):
"""Return the state."""
return self._state
@property
def unique_id(self):
"""Return a unique, Home Assistant friendly identifier for this entity."""
return f"{self._config_entry.unique_id}_{self._locale}_{self._kind}"
@callback @callback
def update_from_latest_data(self): def update_from_latest_data(self) -> None:
"""Update the entity from the latest data.""" """Update the entity from the latest data."""
try: try:
data = self.coordinator.data["current"]["pollution"] data = self.coordinator.data["current"]["pollution"]
@ -172,17 +232,17 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
if self._kind == SENSOR_KIND_LEVEL: if self._kind == SENSOR_KIND_LEVEL:
aqi = data[f"aqi{self._locale}"] aqi = data[f"aqi{self._locale}"]
[(self._state, self._attr_icon)] = [ [(self._attr_state, self._attr_icon)] = [
(name, icon) (name, icon)
for (floor, ceiling), (name, icon) in POLLUTANT_LEVELS.items() for (floor, ceiling), (name, icon) in POLLUTANT_LEVELS.items()
if floor <= aqi <= ceiling if floor <= aqi <= ceiling
] ]
elif self._kind == SENSOR_KIND_AQI: elif self._kind == SENSOR_KIND_AQI:
self._state = data[f"aqi{self._locale}"] self._attr_state = data[f"aqi{self._locale}"]
elif self._kind == SENSOR_KIND_POLLUTANT: elif self._kind == SENSOR_KIND_POLLUTANT:
symbol = data[f"main{self._locale}"] symbol = data[f"main{self._locale}"]
self._state = POLLUTANT_LABELS[symbol] self._attr_state = symbol
self._attrs.update( self._attr_extra_state_attributes.update(
{ {
ATTR_POLLUTANT_SYMBOL: symbol, ATTR_POLLUTANT_SYMBOL: symbol,
ATTR_POLLUTANT_UNIT: POLLUTANT_UNITS[symbol], ATTR_POLLUTANT_UNIT: POLLUTANT_UNITS[symbol],
@ -206,33 +266,43 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
) )
if self._config_entry.options[CONF_SHOW_ON_MAP]: if self._config_entry.options[CONF_SHOW_ON_MAP]:
self._attrs[ATTR_LATITUDE] = latitude self._attr_extra_state_attributes[ATTR_LATITUDE] = latitude
self._attrs[ATTR_LONGITUDE] = longitude self._attr_extra_state_attributes[ATTR_LONGITUDE] = longitude
self._attrs.pop("lati", None) self._attr_extra_state_attributes.pop("lati", None)
self._attrs.pop("long", None) self._attr_extra_state_attributes.pop("long", None)
else: else:
self._attrs["lati"] = latitude self._attr_extra_state_attributes["lati"] = latitude
self._attrs["long"] = longitude self._attr_extra_state_attributes["long"] = longitude
self._attrs.pop(ATTR_LATITUDE, None) self._attr_extra_state_attributes.pop(ATTR_LATITUDE, None)
self._attrs.pop(ATTR_LONGITUDE, None) self._attr_extra_state_attributes.pop(ATTR_LONGITUDE, None)
class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): class AirVisualNodeProSensor(AirVisualEntity, SensorEntity):
"""Define an AirVisual sensor related to a Node/Pro unit.""" """Define an AirVisual sensor related to a Node/Pro unit."""
def __init__(self, coordinator, kind, name, device_class, unit): def __init__(
self,
coordinator: DataUpdateCoordinator,
kind: str,
name: str,
device_class: str | None,
icon: str | None,
unit: str,
) -> None:
"""Initialize.""" """Initialize."""
super().__init__(coordinator) super().__init__(coordinator)
self._kind = kind
self._name = name
self._state = None
self._attr_device_class = device_class self._attr_device_class = device_class
self._attr_icon = icon
self._attr_name = (
f"{coordinator.data['settings']['node_name']} Node/Pro: {name}"
)
self._attr_unique_id = f"{coordinator.data['serial_number']}_{kind}"
self._attr_unit_of_measurement = unit self._attr_unit_of_measurement = unit
self._kind = kind
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity.""" """Return device registry information for this entity."""
return { return {
"identifiers": {(DOMAIN, self.coordinator.data["serial_number"])}, "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])},
@ -245,28 +315,29 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity):
), ),
} }
@property
def name(self):
"""Return the name."""
node_name = self.coordinator.data["settings"]["node_name"]
return f"{node_name} Node/Pro: {self._name}"
@property
def state(self):
"""Return the state."""
return self._state
@property
def unique_id(self):
"""Return a unique, Home Assistant friendly identifier for this entity."""
return f"{self.coordinator.data['serial_number']}_{self._kind}"
@callback @callback
def update_from_latest_data(self): def update_from_latest_data(self) -> None:
"""Update the entity from the latest data.""" """Update the entity from the latest data."""
if self._kind == SENSOR_KIND_BATTERY_LEVEL: if self._kind == SENSOR_KIND_AQI:
self._state = self.coordinator.data["status"]["battery"] if self.coordinator.data["settings"]["is_aqi_usa"]:
self._attr_state = self.coordinator.data["measurements"]["aqi_us"]
else:
self._attr_state = self.coordinator.data["measurements"]["aqi_cn"]
elif self._kind == SENSOR_KIND_BATTERY_LEVEL:
self._attr_state = self.coordinator.data["status"]["battery"]
elif self._kind == SENSOR_KIND_CO2:
self._attr_state = self.coordinator.data["measurements"].get("co2")
elif self._kind == SENSOR_KIND_HUMIDITY: elif self._kind == SENSOR_KIND_HUMIDITY:
self._state = self.coordinator.data["measurements"].get("humidity") self._attr_state = self.coordinator.data["measurements"].get("humidity")
elif self._kind == SENSOR_KIND_PM_0_1:
self._attr_state = self.coordinator.data["measurements"].get("pm0_1")
elif self._kind == SENSOR_KIND_PM_1_0:
self._attr_state = self.coordinator.data["measurements"].get("pm1_0")
elif self._kind == SENSOR_KIND_PM_2_5:
self._attr_state = self.coordinator.data["measurements"].get("pm2_5")
elif self._kind == SENSOR_KIND_TEMPERATURE: elif self._kind == SENSOR_KIND_TEMPERATURE:
self._state = self.coordinator.data["measurements"].get("temperature_C") self._attr_state = self.coordinator.data["measurements"].get(
"temperature_C"
)
elif self._kind == SENSOR_KIND_VOC:
self._attr_state = self.coordinator.data["measurements"].get("voc")

View File

@ -0,0 +1,20 @@
{
"state": {
"airvisual__pollutant_label": {
"co": "Carbon Monoxide",
"n2": "Nitrogen Dioxide",
"o3": "Ozone",
"p1": "PM10",
"p2": "PM2.5",
"s2": "Sulfur Dioxide"
},
"airvisual__pollutant_level": {
"good": "Good",
"moderate": "Moderate",
"unhealthy": "Unhealthy",
"unhealthy_sensitive": "Unhealthy for sensitive groups",
"very_unhealthy": "Very unhealthy",
"hazardous": "Hazardous"
}
}
}

View File

@ -1,7 +1,7 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert.", "already_configured": "Diese Node/Pro ID oder Standort ist bereits konfiguriert.",
"reauth_successful": "Die erneute Authentifizierung war erfolgreich" "reauth_successful": "Die erneute Authentifizierung war erfolgreich"
}, },
"error": { "error": {
@ -35,18 +35,18 @@
"ip_address": "Host", "ip_address": "Host",
"password": "Passwort" "password": "Passwort"
}, },
"description": "\u00dcberwachen Sie eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.", "description": "\u00dcberwache eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.",
"title": "Konfigurieren Sie einen AirVisual Node/Pro" "title": "Konfiguriere einen AirVisual Node/Pro"
}, },
"reauth_confirm": { "reauth_confirm": {
"data": { "data": {
"api_key": "API-Key" "api_key": "API-Schl\u00fcssel"
}, },
"title": "AirVisual erneut authentifizieren" "title": "AirVisual erneut authentifizieren"
}, },
"user": { "user": {
"description": "W\u00e4hlen Sie aus, welche Art von AirVisual-Daten Sie \u00fcberwachen m\u00f6chten.", "description": "W\u00e4hle aus, welche Art von AirVisual-Daten du \u00fcberwachen m\u00f6chtest.",
"title": "Konfigurieren Sie AirVisual" "title": "Konfiguriere AirVisual"
} }
} }
}, },
@ -54,9 +54,9 @@
"step": { "step": {
"init": { "init": {
"data": { "data": {
"show_on_map": "Zeigen Sie die \u00fcberwachte Geografie auf der Karte an" "show_on_map": "Zeige die \u00fcberwachte Geografie auf der Karte an"
}, },
"title": "Konfigurieren Sie AirVisual" "title": "Konfiguriere AirVisual"
} }
} }
} }

View File

@ -6,7 +6,7 @@
"error": { "error": {
"cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4",
"general_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4", "general_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4",
"invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05e1\u05d5\u05e4\u05e7", "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9",
"location_not_found": "\u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0" "location_not_found": "\u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0"
}, },
"step": { "step": {
@ -35,5 +35,14 @@
} }
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"show_on_map": "\u05d4\u05e6\u05d2 \u05d2\u05d9\u05d0\u05d5\u05d2\u05e8\u05e4\u05d9\u05d4 \u05de\u05e0\u05d5\u05d8\u05e8\u05ea \u05d1\u05de\u05e4\u05d4"
}
}
}
} }
} }

View File

@ -7,7 +7,8 @@
"error": { "error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s", "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"general_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", "general_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt",
"invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs",
"location_not_found": "A hely nem tal\u00e1lhat\u00f3"
}, },
"step": { "step": {
"geography_by_coords": { "geography_by_coords": {
@ -15,14 +16,19 @@
"api_key": "API kulcs", "api_key": "API kulcs",
"latitude": "Sz\u00e9less\u00e9g", "latitude": "Sz\u00e9less\u00e9g",
"longitude": "Hossz\u00fas\u00e1g" "longitude": "Hossz\u00fas\u00e1g"
} },
"description": "Haszn\u00e1lja az AirVisual felh\u0151 API-t a sz\u00e9less\u00e9g / hossz\u00fas\u00e1g figyel\u00e9s\u00e9hez.",
"title": "Konfigur\u00e1lja a geogr\u00e1fi\u00e1t"
}, },
"geography_by_name": { "geography_by_name": {
"data": { "data": {
"api_key": "API kulcs", "api_key": "API kulcs",
"city": "V\u00e1ros", "city": "V\u00e1ros",
"country": "Orsz\u00e1g" "country": "Orsz\u00e1g",
} "state": "\u00e1llapot"
},
"description": "Haszn\u00e1lja az AirVisual felh\u0151 API-t egy v\u00e1ros / \u00e1llam / orsz\u00e1g figyel\u00e9s\u00e9hez.",
"title": "Konfigur\u00e1lja a geogr\u00e1fi\u00e1t"
}, },
"node_pro": { "node_pro": {
"data": { "data": {
@ -33,7 +39,8 @@
"reauth_confirm": { "reauth_confirm": {
"data": { "data": {
"api_key": "API kulcs" "api_key": "API kulcs"
} },
"title": "Az AirVisual \u00fajb\u00f3li hiteles\u00edt\u00e9se"
} }
} }
} }

View File

@ -17,7 +17,7 @@
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430"
}, },
"description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0448\u0438\u0440\u043e\u0442\u044b/\u0434\u043e\u043b\u0433\u043e\u0442\u044b.", "description": "\u0414\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u043f\u043e \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual.",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
}, },
"geography_by_name": { "geography_by_name": {
@ -25,9 +25,9 @@
"api_key": "\u041a\u043b\u044e\u0447 API", "api_key": "\u041a\u043b\u044e\u0447 API",
"city": "\u0413\u043e\u0440\u043e\u0434", "city": "\u0413\u043e\u0440\u043e\u0434",
"country": "\u0421\u0442\u0440\u0430\u043d\u0430", "country": "\u0421\u0442\u0440\u0430\u043d\u0430",
"state": "\u0448\u0442\u0430\u0442" "state": "\u0428\u0442\u0430\u0442"
}, },
"description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b.", "description": "\u0414\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual.",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
}, },
"node_pro": { "node_pro": {

View File

@ -0,0 +1,20 @@
{
"state": {
"airvisual__pollutant_label": {
"co": "Mon\u00f2xid de carboni",
"n2": "Di\u00f2xid de nitrogen",
"o3": "Oz\u00f3",
"p1": "PM10",
"p2": "PM2.5",
"s2": "Di\u00f2xid de sofre"
},
"airvisual__pollutant_level": {
"good": "Bo",
"hazardous": "Perill\u00f3s",
"moderate": "Moderat",
"unhealthy": "Poc saludable",
"unhealthy_sensitive": "Poc saludable per a grups sensibles",
"very_unhealthy": "Molt poc saludable"
}
}
}

View File

@ -0,0 +1,20 @@
{
"state": {
"airvisual__pollutant_label": {
"co": "Kohlenmonoxid",
"n2": "Stickstoffdioxid",
"o3": "Ozon",
"p1": "PM10",
"p2": "PM2,5",
"s2": "Schwefeldioxid"
},
"airvisual__pollutant_level": {
"good": "Gut",
"hazardous": "Gef\u00e4hrlich",
"moderate": "M\u00e4\u00dfig",
"unhealthy": "Ungesund",
"unhealthy_sensitive": "Ungesund f\u00fcr sensible Gruppen",
"very_unhealthy": "Sehr ungesund"
}
}
}

View File

@ -0,0 +1,20 @@
{
"state": {
"airvisual__pollutant_label": {
"co": "Carbon Monoxide",
"n2": "Nitrogen Dioxide",
"o3": "Ozone",
"p1": "PM10",
"p2": "PM2.5",
"s2": "Sulfur Dioxide"
},
"airvisual__pollutant_level": {
"good": "Good",
"hazardous": "Hazardous",
"moderate": "Moderate",
"unhealthy": "Unhealthy",
"unhealthy_sensitive": "Unhealthy for sensitive groups",
"very_unhealthy": "Very unhealthy"
}
}
}

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