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

View File

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

View File

@ -740,4 +740,4 @@ jobs:
coverage report --fail-under=94
coverage xml
- 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:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2.0.3
- uses: dessant/lock-threads@v2.1.1
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: "30"

View File

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

View File

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

View File

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

View File

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

15
.vscode/tasks.json vendored
View File

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

View File

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

View File

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

View File

@ -146,8 +146,8 @@ def daemonize() -> None:
# redirect standard file descriptors to devnull
# pylint: disable=consider-using-with
infd = open(os.devnull)
outfd = open(os.devnull, "a+")
infd = open(os.devnull, encoding="utf8")
outfd = open(os.devnull, "a+", encoding="utf8")
sys.stdout.flush()
sys.stderr.flush()
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 pid file
try:
with open(pid_file) as file:
with open(pid_file, encoding="utf8") as file:
pid = int(file.readline())
except OSError:
# PID File does not exist
@ -182,7 +182,7 @@ def write_pid(pid_file: str) -> None:
"""Create a PID File."""
pid = os.getpid()
try:
with open(pid_file, "w") as file:
with open(pid_file, "w", encoding="utf8") as file:
file.write(str(pid))
except OSError:
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."""
from __future__ import annotations
import asyncio.subprocess
import asyncio
import collections
from collections.abc import Mapping
import logging
@ -64,7 +64,7 @@ class CommandLineAuthProvider(AuthProvider):
"""Validate a username and password."""
env = {"username": username, "password": password}
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_ARGS],
env=env,

View File

@ -1,5 +1,4 @@
"""Support for the Abode Security System."""
from copy import deepcopy
from functools import partial
from abodepy import Abode
@ -8,7 +7,6 @@ import abodepy.helpers.timeline as TIMELINE
from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DATE,
@ -44,22 +42,7 @@ ATTR_APP_TYPE = "app_type"
ATTR_EVENT_BY = "event_by"
ATTR_VALUE = "value"
CONFIG_SCHEMA = vol.Schema(
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,
)
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
CHANGE_SETTING_SCHEMA = vol.Schema(
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
@ -92,22 +75,6 @@ class AbodeSystem:
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):
"""Set up Abode integration from a config entry."""
username = config_entry.data.get(CONF_USERNAME)
@ -284,17 +251,13 @@ class AbodeEntity(Entity):
"""Initialize Abode entity."""
self._data = data
self._available = True
self._attr_should_poll = data.polling
@property
def available(self):
"""Return the available state."""
return self._available
@property
def should_poll(self):
"""Return the polling state."""
return self._data.polling
async def async_added_to_hass(self):
"""Subscribe to Abode connection status updates."""
await self.hass.async_add_executor_job(
@ -324,6 +287,8 @@ class AbodeDevice(AbodeEntity):
"""Initialize Abode device."""
super().__init__(data)
self._device = device
self._attr_name = device.name
self._attr_unique_id = device.device_uuid
async def async_added_to_hass(self):
"""Subscribe to device events."""
@ -345,11 +310,6 @@ class AbodeDevice(AbodeEntity):
"""Update device state."""
self._device.refresh()
@property
def name(self):
"""Return the name of the device."""
return self._device.name
@property
def extra_state_attributes(self):
"""Return the state attributes."""
@ -361,11 +321,6 @@ class AbodeDevice(AbodeEntity):
"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
def device_info(self):
"""Return device registry information for this entity."""
@ -388,22 +343,13 @@ class AbodeAutomation(AbodeEntity):
"""Initialize for Abode automation."""
super().__init__(data)
self._automation = automation
self._attr_name = automation.name
self._attr_unique_id = automation.automation_id
self._attr_extra_state_attributes = {
ATTR_ATTRIBUTION: ATTRIBUTION,
"type": "CUE automation",
}
def update(self):
"""Update automation state."""
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):
"""An alarm_control_panel implementation for Abode."""
@property
def icon(self):
"""Return the icon."""
return ICON
_attr_icon = ICON
_attr_code_arm_required = False
_attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY
@property
def state(self):
@ -46,16 +45,6 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanelEntity):
state = None
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):
"""Send disarm command."""
self._device.set_standby()

View File

@ -158,13 +158,3 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._password = user_input[CONF_PASSWORD]
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."""
super().__init__(data, device)
self._sensor_type = sensor_type
self._name = f"{self._device.name} {SENSOR_TYPES[self._sensor_type][0]}"
self._device_class = SENSOR_TYPES[self._sensor_type][1]
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
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}"
self._attr_name = f"{device.name} {SENSOR_TYPES[sensor_type][0]}"
self._attr_device_class = SENSOR_TYPES[self._sensor_type][1]
self._attr_unique_id = f"{device.device_uuid}-{sensor_type}"
if self._sensor_type == CONST.TEMP_STATUS_KEY:
self._attr_unit_of_measurement = device.temp_unit
elif self._sensor_type == CONST.HUMI_STATUS_KEY:
self._attr_unit_of_measurement = device.humidity_unit
elif self._sensor_type == CONST.LUX_STATUS_KEY:
self._attr_unit_of_measurement = device.lux_unit
@property
def state(self):
@ -68,13 +60,3 @@ class AbodeSensor(AbodeDevice, SensorEntity):
return self._device.humidity
if self._sensor_type == CONST.LUX_STATUS_KEY:
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):
"""A switch implementation for Abode automations."""
_attr_icon = ICON
async def async_added_to_hass(self):
"""Set up trigger automation service."""
await super().async_added_to_hass()
@ -73,8 +75,3 @@ class AbodeAutomationSwitch(AbodeAutomation, SwitchEntity):
def is_on(self):
"""Return True if the 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": {
"data": {
"password": "Passwort",
"username": "E-Mail-Adresse"
"username": "E-Mail"
},
"title": "Gib deine Abode-Anmeldeinformationen ein"
}

View File

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

View File

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

View File

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

View File

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

View File

@ -3,17 +3,10 @@ from __future__ import annotations
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.const import (
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
ATTR_ICON,
CONF_NAME,
DEVICE_CLASS_TEMPERATURE,
)
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TEMPERATURE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -22,11 +15,7 @@ from . import AccuWeatherDataUpdateCoordinator
from .const import (
API_IMPERIAL,
API_METRIC,
ATTR_ENABLED,
ATTR_FORECAST,
ATTR_LABEL,
ATTR_UNIT_IMPERIAL,
ATTR_UNIT_METRIC,
ATTRIBUTION,
DOMAIN,
FORECAST_SENSOR_TYPES,
@ -35,6 +24,7 @@ from .const import (
NAME,
SENSOR_TYPES,
)
from .model import AccuWeatherSensorDescription
PARALLEL_UPDATES = 1
@ -48,17 +38,19 @@ async def async_setup_entry(
coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
sensors: list[AccuWeatherSensor] = []
for sensor in SENSOR_TYPES:
sensors.append(AccuWeatherSensor(name, sensor, coordinator))
for description in SENSOR_TYPES:
sensors.append(AccuWeatherSensor(name, coordinator, description))
if coordinator.forecast:
for sensor in FORECAST_SENSOR_TYPES:
for description in FORECAST_SENSOR_TYPES:
for day in range(MAX_FORECAST_DAYS + 1):
# Some air quality/allergy sensors are only available for certain
# locations.
if sensor in coordinator.data[ATTR_FORECAST][0]:
if description.key in coordinator.data[ATTR_FORECAST][0]:
sensors.append(
AccuWeatherSensor(name, sensor, coordinator, forecast_day=day)
AccuWeatherSensor(
name, coordinator, description, forecast_day=day
)
)
async_add_entities(sensors)
@ -68,119 +60,107 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity):
"""Define an AccuWeather entity."""
coordinator: AccuWeatherDataUpdateCoordinator
entity_description: AccuWeatherSensorDescription
def __init__(
self,
name: str,
kind: str,
coordinator: AccuWeatherDataUpdateCoordinator,
description: AccuWeatherSensorDescription,
forecast_day: int | None = None,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._sensor_data = _get_sensor_data(coordinator.data, forecast_day, kind)
if forecast_day is None:
self._description = SENSOR_TYPES[kind]
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.entity_description = description
self._sensor_data = _get_sensor_data(
coordinator.data, forecast_day, description.key
)
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
self.forecast_day = forecast_day
self._attr_state_class = self._description.get(ATTR_STATE_CLASS)
@property
def name(self) -> str:
"""Return the name."""
if self.forecast_day is not None:
return f"{self._name} {self._description[ATTR_LABEL]} {self.forecast_day}d"
return f"{self._name} {self._description[ATTR_LABEL]}"
@property
def unique_id(self) -> str:
"""Return a unique_id for this entity."""
if self.forecast_day is not None:
return f"{self.coordinator.location_key}-{self.kind}-{self.forecast_day}".lower()
return f"{self.coordinator.location_key}-{self.kind}".lower()
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return {
"identifiers": {(DOMAIN, self.coordinator.location_key)},
if forecast_day is not None:
self._attr_name = f"{name} {description.name} {forecast_day}d"
self._attr_unique_id = (
f"{coordinator.location_key}-{description.key}-{forecast_day}".lower()
)
else:
self._attr_name = f"{name} {description.name}"
self._attr_unique_id = (
f"{coordinator.location_key}-{description.key}".lower()
)
if coordinator.is_metric:
self._unit_system = API_METRIC
self._attr_unit_of_measurement = description.unit_metric
else:
self._unit_system = API_IMPERIAL
self._attr_unit_of_measurement = description.unit_imperial
self._attr_device_info = {
"identifiers": {(DOMAIN, coordinator.location_key)},
"name": NAME,
"manufacturer": MANUFACTURER,
"entry_type": "service",
}
self.forecast_day = forecast_day
@property
def state(self) -> StateType:
"""Return the state."""
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"])
if self.kind == "UVIndex":
if self.entity_description.key == "UVIndex":
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"])
if self.kind == "Ceiling":
if self.entity_description.key == "Ceiling":
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())
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"])
if self.kind == "Precipitation":
if self.entity_description.key == "Precipitation":
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"])
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)
@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
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
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"]
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"]
return self._attrs
if self.kind == "UVIndex":
if self.entity_description.key == "UVIndex":
self._attrs["level"] = self.coordinator.data["UVIndexText"]
elif self.kind == "Precipitation":
elif self.entity_description.key == "Precipitation":
self._attrs["type"] = self.coordinator.data["PrecipitationType"]
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
def _handle_coordinator_update(self) -> None:
"""Handle data update."""
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()

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": {
"cannot_connect": "Verbindung fehlgeschlagen",
"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": {
"user": {

View File

@ -1,6 +1,11 @@
{
"config": {
"abort": {
"single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n."
},
"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."
},
"step": {

View File

@ -14,8 +14,22 @@
"latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1",
"longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da",
"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"
}
}
},
"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.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utc_from_timestamp
@ -60,29 +59,15 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._name = name
self._unit_system = API_METRIC if self.coordinator.is_metric else API_IMPERIAL
@property
def name(self) -> str:
"""Return the name."""
return self._name
@property
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)},
self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL
self._attr_name = name
self._attr_unique_id = coordinator.location_key
self._attr_temperature_unit = (
TEMP_CELSIUS if coordinator.is_metric else TEMP_FAHRENHEIT
)
self._attr_attribution = ATTRIBUTION
self._attr_device_info = {
"identifiers": {(DOMAIN, coordinator.location_key)},
"name": NAME,
"manufacturer": MANUFACTURER,
"entry_type": "service",
@ -107,11 +92,6 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity):
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
def pressure(self) -> float:
"""Return the pressure."""

View File

@ -67,6 +67,8 @@ def setup_platform(
class AcerSwitch(SwitchEntity):
"""Represents an Acer Projector as a switch."""
_attr_icon = ICON
def __init__(
self,
serial_port: str,
@ -79,9 +81,7 @@ class AcerSwitch(SwitchEntity):
port=serial_port, timeout=timeout, write_timeout=write_timeout
)
self._serial_port = serial_port
self._name = name
self._state = False
self._available = False
self._attr_name = name
self._attributes = {
LAMP_HOURS: STATE_UNKNOWN,
INPUT_SOURCE: STATE_UNKNOWN,
@ -116,57 +116,33 @@ class AcerSwitch(SwitchEntity):
return match.group(1)
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:
"""Get the latest state from the projector."""
awns = self._write_read_format(CMD_DICT[LAMP])
if awns == "Lamp 1":
self._state = True
self._available = True
self._attr_is_on = True
self._attr_available = True
elif awns == "Lamp 0":
self._state = False
self._available = True
self._attr_is_on = False
self._attr_available = True
else:
self._available = False
self._attr_available = False
for key in self._attributes:
msg = CMD_DICT.get(key)
if msg:
awns = self._write_read_format(msg)
self._attributes[key] = awns
self._attr_extra_state_attributes = self._attributes
def turn_on(self, **kwargs: Any) -> None:
"""Turn the projector on."""
msg = CMD_DICT[STATE_ON]
self._write_read(msg)
self._state = True
self._attr_is_on = True
def turn_off(self, **kwargs: Any) -> None:
"""Turn the projector off."""
msg = CMD_DICT[STATE_OFF]
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",
"password": "Passwort",
"port": "Port",
"ssl": "AdGuard Home verwendet ein SSL-Zertifikat",
"ssl": "Verwendet ein SSL-Zertifikat",
"username": "Benutzername",
"verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen"
},

View File

@ -1,9 +1,16 @@
{
"config": {
"abort": {
"already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van"
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"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": {
"data": {
"host": "Hoszt",

View File

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

View File

@ -268,15 +268,17 @@ class AdsHub:
class AdsEntity(Entity):
"""Representation of ADS entity."""
_attr_should_poll = False
def __init__(self, ads_hub, name, ads_var):
"""Initialize ADS binary sensor."""
self._name = name
self._unique_id = ads_var
self._state_dict = {}
self._state_dict[STATE_KEY_STATE] = None
self._ads_hub = ads_hub
self._ads_var = ads_var
self._event = None
self._attr_unique_id = ads_var
self._attr_name = name
async def async_initialize_device(
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)
@property
def name(self):
"""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):
def available(self) -> bool:
"""Return False if state has not been updated yet."""
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):
"""Initialize ADS binary sensor."""
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):
"""Register device notification."""
await self.async_initialize_device(self._ads_var, self._ads_hub.PLCTYPE_BOOL)
@property
def is_on(self):
def is_on(self) -> bool:
"""Return True if the entity is on."""
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_close = ads_var_close
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):
"""Register device notification."""
@ -119,11 +124,6 @@ class AdsCover(AdsEntity, CoverEntity):
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
def is_closed(self):
"""Return if the cover is closed."""
@ -138,19 +138,6 @@ class AdsCover(AdsEntity, CoverEntity):
"""Return current position of cover."""
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):
"""Fire the stop action."""
if self._ads_var_stop:
@ -185,7 +172,7 @@ class AdsCover(AdsEntity, CoverEntity):
self.set_cover_position(position=0)
@property
def available(self):
def available(self) -> bool:
"""Return False if state has not been updated yet."""
if self._ads_var is not None or self._ads_var_position is not None:
return (

View File

@ -1,4 +1,6 @@
"""Support for ADS light sources."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.components.light import (
@ -48,6 +50,8 @@ class AdsLight(AdsEntity, LightEntity):
super().__init__(ads_hub, name, ads_var_enable)
self._state_dict[STATE_KEY_BRIGHTNESS] = None
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):
"""Register device notification."""
@ -61,19 +65,12 @@ class AdsLight(AdsEntity, LightEntity):
)
@property
def brightness(self):
def brightness(self) -> int | None:
"""Return the brightness of the light (0..255)."""
return self._state_dict[STATE_KEY_BRIGHTNESS]
@property
def supported_features(self):
"""Flag supported features."""
if self._ads_var_brightness is not None:
return SUPPORT_BRIGHTNESS
return 0
@property
def is_on(self):
def is_on(self) -> bool:
"""Return True if the entity is on."""
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.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
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
@ -49,7 +50,7 @@ class AdsSensor(AdsEntity, SensorEntity):
def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor):
"""Initialize AdsSensor entity."""
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._factor = factor
@ -63,11 +64,6 @@ class AdsSensor(AdsEntity, SensorEntity):
)
@property
def state(self):
def state(self) -> StateType:
"""Return the state of the device."""
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)
@property
def is_on(self):
def is_on(self) -> bool:
"""Return True if the entity is on."""
return self._state_dict[STATE_KEY_STATE]

View File

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

View File

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

View File

@ -14,6 +14,13 @@ class AdvantageAirEntity(CoordinatorEntity):
self.async_change = instance["async_change"]
self.ac_key = ac_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
def _ac(self):
@ -22,14 +29,3 @@ class AdvantageAirEntity(CoordinatorEntity):
@property
def _zone(self):
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",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/advantage_air",
"codeowners": ["@Bre77"],
"requirements": ["advantage_air==0.2.1"],
"codeowners": [
"@Bre77"
],
"requirements": [
"advantage_air==0.2.5"
],
"quality_scale": "platinum",
"iot_class": "local_polling"
}

View File

@ -1,8 +1,8 @@
"""Sensor platform for Advantage Air integration."""
import voluptuous as vol
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import PERCENTAGE
from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
from homeassistant.helpers import config_validation as cv, entity_platform
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, "Off"))
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:
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
if zone["rssi"] > 0:
entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key))
@ -50,17 +51,11 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity):
"""Initialize the Advantage Air timer control."""
super().__init__(instance, ac_key)
self.action = action
self._time_key = f"countDownTo{self.action}"
@property
def name(self):
"""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}'
self._time_key = f"countDownTo{action}"
self._attr_name = f'{self._ac["name"]} Time To {action}'
self._attr_unique_id = (
f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{action}'
)
@property
def state(self):
@ -84,16 +79,15 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air Zone Vent Sensor."""
_attr_unit_of_measurement = PERCENTAGE
_attr_state_class = STATE_CLASS_MEASUREMENT
@property
def name(self):
"""Return the name."""
return f'{self._zone["name"]} Vent'
@property
def unique_id(self):
"""Return a unique id."""
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-vent'
def __init__(self, instance, ac_key, zone_key):
"""Initialize an Advantage Air Zone Vent Sensor."""
super().__init__(instance, ac_key, zone_key=zone_key)
self._attr_name = f'{self._zone["name"]} Vent'
self._attr_unique_id = (
f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-vent'
)
@property
def state(self):
@ -114,16 +108,15 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
"""Representation of Advantage Air Zone wireless signal sensor."""
_attr_unit_of_measurement = PERCENTAGE
_attr_state_class = STATE_CLASS_MEASUREMENT
@property
def name(self):
"""Return the name."""
return f'{self._zone["name"]} Signal'
@property
def unique_id(self):
"""Return a unique id."""
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-{self.zone_key}-signal'
def __init__(self, instance, ac_key, zone_key):
"""Initialize an Advantage Air Zone wireless signal sensor."""
super().__init__(instance, ac_key, zone_key=zone_key)
self._attr_name = f'{self._zone["name"]} Signal'
self._attr_unique_id = (
f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-signal'
)
@property
def state(self):
@ -142,3 +135,23 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity):
if self._zone["rssi"] >= 20:
return "mdi:wifi-strength-1"
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):
"""Representation of Advantage Air fresh air control."""
@property
def name(self):
"""Return the name."""
return f'{self._ac["name"]} Fresh Air'
_attr_icon = "mdi:air-filter"
@property
def unique_id(self):
"""Return a unique id."""
return f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-freshair'
def __init__(self, instance, ac_key):
"""Initialize an Advantage Air fresh air control."""
super().__init__(instance, ac_key)
self._attr_name = f'{self._ac["name"]} Fresh Air'
self._attr_unique_id = (
f'{self.coordinator.data["system"]["rid"]}-{ac_key}-freshair'
)
@property
def is_on(self):
"""Return the fresh air status."""
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):
"""Turn fresh air on."""
await self.async_change(

View File

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

View File

@ -66,6 +66,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AbstractAemetSensor(CoordinatorEntity, SensorEntity):
"""Abstract class for an AEMET OpenData sensor."""
_attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
def __init__(
self,
name,
@ -80,33 +82,10 @@ class AbstractAemetSensor(CoordinatorEntity, SensorEntity):
self._unique_id = unique_id
self._sensor_type = sensor_type
self._sensor_name = sensor_configuration[SENSOR_NAME]
self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT)
self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS)
@property
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}
self._attr_name = f"{self._name} {self._sensor_name}"
self._attr_unique_id = self._unique_id
self._attr_device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS)
self._attr_unit_of_measurement = sensor_configuration.get(SENSOR_UNIT)
class AemetSensor(AbstractAemetSensor):
@ -150,11 +129,9 @@ class AemetForecastSensor(AbstractAemetSensor):
)
self._weather_coordinator = weather_coordinator
self._forecast_mode = forecast_mode
@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
self._attr_entity_registry_enabled_default = (
self._forecast_mode == FORECAST_MODE_DAILY
)
@property
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"
},
"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"
}
}
},
"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",
"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"
}
}
},
"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"
}
}
},
"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):
"""Implementation of an AEMET OpenData sensor."""
_attr_attribution = ATTRIBUTION
_attr_temperature_unit = TEMP_CELSIUS
def __init__(
self,
name,
@ -48,25 +51,18 @@ class AemetWeather(CoordinatorEntity, WeatherEntity):
):
"""Initialize the sensor."""
super().__init__(coordinator)
self._name = name
self._unique_id = unique_id
self._forecast_mode = forecast_mode
@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION
self._attr_entity_registry_enabled_default = (
self._forecast_mode == FORECAST_MODE_DAILY
)
self._attr_name = name
self._attr_unique_id = unique_id
@property
def condition(self):
"""Return the current 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
def forecast(self):
"""Return the forecast array."""
@ -77,11 +73,6 @@ class AemetWeather(CoordinatorEntity, WeatherEntity):
"""Return the humidity."""
return self.coordinator.data[ATTR_API_HUMIDITY]
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def pressure(self):
"""Return the pressure."""
@ -92,16 +83,6 @@ class AemetWeather(CoordinatorEntity, WeatherEntity):
"""Return the 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
def wind_bearing(self):
"""Return the temperature."""

View File

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

View File

@ -35,90 +35,60 @@ async def async_setup_entry(
class AgentBaseStation(AlarmControlPanelEntity):
"""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):
"""Initialize the alarm control panel."""
self._state = None
self._client = client
self._unique_id = f"{client.unique}_CP"
name = CONST_ALARM_CONTROL_PANEL_NAME
self._name = name = f"{client.name} {name}"
@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)},
self._attr_name = f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}"
self._attr_unique_id = f"{client.unique}_CP"
self._attr_device_info = {
"identifiers": {(AGENT_DOMAIN, client.unique)},
"manufacturer": "Agent",
"model": CONST_ALARM_CONTROL_PANEL_NAME,
"sw_version": self._client.version,
"sw_version": client.version,
}
async def async_update(self):
"""Update the state of the device."""
await self._client.update()
self._attr_available = self._client.is_available
armed = self._client.is_armed
if armed is None:
self._state = None
self._attr_state = None
return
if armed:
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:
self._state = STATE_ALARM_ARMED_HOME
self._attr_state = STATE_ALARM_ARMED_HOME
elif prof == CONF_NIGHT_MODE_NAME:
self._state = STATE_ALARM_ARMED_NIGHT
self._attr_state = STATE_ALARM_ARMED_NIGHT
else:
self._state = STATE_ALARM_DISARMED
self._attr_state = STATE_ALARM_DISARMED
async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
await self._client.disarm()
self._state = STATE_ALARM_DISARMED
self._attr_state = STATE_ALARM_DISARMED
async def async_alarm_arm_away(self, code=None):
"""Send arm away command. Uses custom mode."""
await self._client.arm()
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):
"""Send arm home command. Uses custom mode."""
await self._client.arm()
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):
"""Send arm night command. Uses custom mode."""
await self._client.arm()
await self._client.set_active_profile(CONF_NIGHT_MODE_NAME)
self._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
self._attr_state = STATE_ALARM_ARMED_NIGHT

View File

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

View File

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

View File

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

View File

@ -3,16 +3,10 @@ from __future__ import annotations
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.const import (
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
ATTR_ICON,
CONF_NAME,
)
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -27,12 +21,9 @@ from .const import (
ATTR_API_PM10,
ATTR_API_PM25,
ATTR_DESCRIPTION,
ATTR_LABEL,
ATTR_LEVEL,
ATTR_LIMIT,
ATTR_PERCENT,
ATTR_UNIT,
ATTR_VALUE,
ATTRIBUTION,
DEFAULT_NAME,
DOMAIN,
@ -41,6 +32,7 @@ from .const import (
SUFFIX_LIMIT,
SUFFIX_PERCENT,
)
from .model import AirlySensorEntityDescription
PARALLEL_UPDATES = 1
@ -54,10 +46,10 @@ async def async_setup_entry(
coordinator = hass.data[DOMAIN][entry.entry_id]
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
if coordinator.data.get(sensor):
sensors.append(AirlySensor(coordinator, name, sensor))
if coordinator.data.get(description.key):
sensors.append(AirlySensor(coordinator, name, description))
async_add_entities(sensors, False)
@ -66,47 +58,54 @@ class AirlySensor(CoordinatorEntity, SensorEntity):
"""Define an Airly sensor."""
coordinator: AirlyDataUpdateCoordinator
entity_description: AirlySensorEntityDescription
def __init__(
self, coordinator: AirlyDataUpdateCoordinator, name: str, kind: str
self,
coordinator: AirlyDataUpdateCoordinator,
name: str,
description: AirlySensorEntityDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._description = description = SENSOR_TYPES[kind]
self._attr_device_class = description.get(ATTR_DEVICE_CLASS)
self._attr_icon = description.get(ATTR_ICON)
self._attr_name = f"{name} {description[ATTR_LABEL]}"
self._attr_state_class = description.get(ATTR_STATE_CLASS)
self._attr_device_info = {
"identifiers": {
(DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}")
},
"name": DEFAULT_NAME,
"manufacturer": MANUFACTURER,
"entry_type": "service",
}
self._attr_name = f"{name} {description.name}"
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.kind = kind
self.entity_description = description
@property
def state(self) -> StateType:
"""Return the state."""
state = self.coordinator.data[self.kind]
return cast(StateType, self._description[ATTR_VALUE](state))
state = self.coordinator.data[self.entity_description.key]
return cast(StateType, self.entity_description.value(state))
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""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_ADVICE] = self.coordinator.data[ATTR_API_ADVICE]
self._attrs[ATTR_DESCRIPTION] = self.coordinator.data[
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[
f"{ATTR_API_PM25}_{SUFFIX_LIMIT}"
]
self._attrs[ATTR_PERCENT] = round(
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[
f"{ATTR_API_PM10}_{SUFFIX_LIMIT}"
]
@ -114,18 +113,3 @@ class AirlySensor(CoordinatorEntity, SensorEntity):
self.coordinator.data[f"{ATTR_API_PM10}_{SUFFIX_PERCENT}"]
)
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": {
"info": {
"can_reach_server": "\u00c9rje el az Airly szervert",
"requests_per_day": "Enged\u00e9lyezett k\u00e9r\u00e9sek naponta",
"requests_remaining": "Fennmarad\u00f3 enged\u00e9lyezett k\u00e9r\u00e9sek"
}

View File

@ -67,16 +67,13 @@ class AirNowSensor(CoordinatorEntity, SensorEntity):
"""Initialize."""
super().__init__(coordinator)
self.kind = kind
self._device_class = None
self._state = None
self._icon = None
self._unit_of_measurement = None
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
@property
def name(self):
"""Return the name."""
return f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}"
self._attr_name = f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}"
self._attr_icon = SENSOR_TYPES[self.kind][ATTR_ICON]
self._attr_device_class = SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
self._attr_unit_of_measurement = SENSOR_TYPES[self.kind][ATTR_UNIT]
self._attr_unique_id = f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}"
@property
def state(self):
@ -96,24 +93,3 @@ class AirNowSensor(CoordinatorEntity, SensorEntity):
]
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": {
"step": {
"user": {

View File

@ -17,7 +17,7 @@
"longitude": "L\u00e4ngengrad",
"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"
}
}

View File

@ -14,8 +14,10 @@
"data": {
"api_key": "API kulcs",
"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"
}
}

View File

@ -1,6 +1,10 @@
"""The airvisual component."""
from __future__ import annotations
from collections.abc import Mapping
from datetime import timedelta
from math import ceil
from typing import Any
from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import (
@ -10,6 +14,7 @@ from pyairvisual.errors import (
NodeProError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY,
@ -20,9 +25,13 @@ from homeassistant.const import (
CONF_SHOW_ON_MAP,
CONF_STATE,
)
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
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 (
CoordinatorEntity,
DataUpdateCoordinator,
@ -42,7 +51,7 @@ from .const import (
LOGGER,
)
PLATFORMS = ["air_quality", "sensor"]
PLATFORMS = ["sensor"]
DATA_LISTENER = "listener"
@ -53,11 +62,8 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN)
@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."""
if not geography_dict:
return
if CONF_CITY in geography_dict:
return ", ".join(
(
@ -72,7 +78,9 @@ def async_get_geography_id(geography_dict):
@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.
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
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."""
coordinators = []
for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items():
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)
return coordinators
@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)."""
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
async def async_setup(hass, config):
"""Set up the AirVisual component."""
hass.data[DOMAIN] = {DATA_COORDINATOR: {}, DATA_LISTENER: {}}
return True
@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."""
entry_updates = {}
@ -164,9 +172,11 @@ def _standardize_geography_config_entry(hass, config_entry):
@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."""
entry_updates = {}
entry_updates: dict[str, Any] = {}
if CONF_INTEGRATION_TYPE not in config_entry.data:
# 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)
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."""
hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_LISTENER: {}})
if CONF_API_KEY in config_entry.data:
_standardize_geography_config_entry(hass, config_entry)
websession = aiohttp_client.async_get_clientsession(hass)
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."""
if CONF_CITY in config_entry.data:
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.add_update_listener(async_reload_entry)
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)
async def async_update_data():
async def async_update_data() -> dict[str, Any]:
"""Get new data from the API."""
try:
async with NodeSamba(
@ -262,7 +287,7 @@ async def async_setup_entry(hass, config_entry):
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."""
version = config_entry.version
@ -304,7 +329,7 @@ async def async_migrate_entry(hass, config_entry):
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_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
@ -325,7 +350,7 @@ async def async_unload_entry(hass, config_entry):
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."""
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):
"""Define a generic AirVisual entity."""
def __init__(self, coordinator):
def __init__(self, coordinator: DataUpdateCoordinator) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
@property
def extra_state_attributes(self):
"""Return the device state attributes."""
return self._attrs
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
async def async_added_to_hass(self):
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@callback
def update():
def update() -> None:
"""Update the state."""
self.update_from_latest_data()
self.async_write_ha_state()
@ -357,6 +378,6 @@ class AirVisualEntity(CoordinatorEntity):
self.update_from_latest_data()
@callback
def update_from_latest_data(self):
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""
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."""
from __future__ import annotations
import asyncio
from pyairvisual import CloudAPI, NodeSamba
@ -11,6 +13,7 @@ from pyairvisual.errors import (
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry, OptionsFlow
from homeassistant.const import (
CONF_API_KEY,
CONF_IP_ADDRESS,
@ -21,6 +24,7 @@ from homeassistant.const import (
CONF_STATE,
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client, config_validation as cv
from . import async_get_geography_id
@ -64,13 +68,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 2
def __init__(self):
def __init__(self) -> None:
"""Initialize the config flow."""
self._entry_data_for_reauth = None
self._geo_id = None
self._entry_data_for_reauth: dict[str, str] = {}
self._geo_id: str | None = None
@property
def geography_coords_schema(self):
def geography_coords_schema(self) -> vol.Schema:
"""Return the data schema for the cloud API."""
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."""
websession = aiohttp_client.async_get_clientsession(self.hass)
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},
)
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."""
self._geo_id = async_get_geography_id(user_input)
await self._async_set_unique_id(self._geo_id)
self._abort_if_unique_id_configured()
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."""
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
@staticmethod
@callback
def async_get_options_flow(config_entry):
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Define the config flow to handle options."""
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."""
if not user_input:
return self.async_show_form(
@ -171,7 +181,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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."""
if not user_input:
return self.async_show_form(
@ -182,7 +194,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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."""
if not user_input:
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},
)
async def async_step_reauth(self, data):
async def async_step_reauth(self, data: dict[str, str]) -> FlowResult:
"""Handle configuration by re-auth."""
self._entry_data_for_reauth = data
self._geo_id = async_get_geography_id(data)
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."""
if not user_input:
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]
)
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."""
if not user_input:
return self.async_show_form(
@ -244,11 +262,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class AirVisualOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle an AirVisual options flow."""
def __init__(self, config_entry):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize."""
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."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

View File

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

View File

@ -1,5 +1,8 @@
"""Support for AirVisual air quality sensors."""
from __future__ import annotations
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_LATITUDE,
ATTR_LONGITUDE,
@ -12,12 +15,16 @@ from homeassistant.const import (
CONF_SHOW_ON_MAP,
CONF_STATE,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_CO2,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
PERCENTAGE,
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 .const import (
@ -36,12 +43,21 @@ ATTR_POLLUTANT_SYMBOL = "pollutant_symbol"
ATTR_POLLUTANT_UNIT = "pollutant_unit"
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_POLLUTANT = "main_pollutant"
SENSOR_KIND_BATTERY_LEVEL = "battery_level"
SENSOR_KIND_CO2 = "carbon_dioxide"
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_VOC = "voc"
GEOGRAPHY_SENSORS = [
(SENSOR_KIND_LEVEL, "Air Pollution Level", "mdi:gauge", None),
@ -51,27 +67,74 @@ GEOGRAPHY_SENSORS = [
GEOGRAPHY_SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."}
NODE_PRO_SENSORS = [
(SENSOR_KIND_BATTERY_LEVEL, "Battery", DEVICE_CLASS_BATTERY, PERCENTAGE),
(SENSOR_KIND_HUMIDITY, "Humidity", DEVICE_CLASS_HUMIDITY, PERCENTAGE),
(SENSOR_KIND_TEMPERATURE, "Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS),
(SENSOR_KIND_AQI, "Air Quality Index", None, "mdi:chart-line", "AQI"),
(SENSOR_KIND_BATTERY_LEVEL, "Battery", DEVICE_CLASS_BATTERY, None, PERCENTAGE),
(
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 = {
"co": "Carbon Monoxide",
"n2": "Nitrogen Dioxide",
"o3": "Ozone",
"p1": "PM10",
"p2": "PM2.5",
"s2": "Sulfur Dioxide",
}
STATE_POLLUTANT_LABEL_CO = "co"
STATE_POLLUTANT_LABEL_N2 = "n2"
STATE_POLLUTANT_LABEL_O3 = "o3"
STATE_POLLUTANT_LABEL_P1 = "p1"
STATE_POLLUTANT_LABEL_P2 = "p2"
STATE_POLLUTANT_LABEL_S2 = "s2"
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 = {
(0, 50): ("Good", "mdi:emoticon-excited"),
(51, 100): ("Moderate", "mdi:emoticon-happy"),
(101, 150): ("Unhealthy for sensitive groups", "mdi:emoticon-neutral"),
(151, 200): ("Unhealthy", "mdi:emoticon-sad"),
(201, 300): ("Very unhealthy", "mdi:emoticon-dead"),
(301, 1000): ("Hazardous", "mdi:biohazard"),
(0, 50): (STATE_POLLUTANT_LEVEL_GOOD, "mdi:emoticon-excited"),
(51, 100): (STATE_POLLUTANT_LEVEL_MODERATE, "mdi:emoticon-happy"),
(101, 150): (STATE_POLLUTANT_LEVEL_UNHEALTHY_SENSITIVE, "mdi:emoticon-neutral"),
(151, 200): (STATE_POLLUTANT_LEVEL_UNHEALTHY, "mdi:emoticon-sad"),
(201, 300): (STATE_POLLUTANT_LEVEL_VERY_UNHEALTHY, "mdi:emoticon-dead"),
(301, 1000): (STATE_POLLUTANT_LEVEL_HAZARDOUS, "mdi:biohazard"),
}
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."""
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
sensors: list[AirVisualGeographySensor | AirVisualNodeProSensor]
if config_entry.data[CONF_INTEGRATION_TYPE] in [
INTEGRATION_TYPE_GEOGRAPHY_COORDS,
INTEGRATION_TYPE_GEOGRAPHY_NAME,
@ -107,8 +175,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
]
else:
sensors = [
AirVisualNodeProSensor(coordinator, kind, name, device_class, unit)
for kind, name, device_class, unit in NODE_PRO_SENSORS
AirVisualNodeProSensor(coordinator, kind, name, device_class, icon, unit)
for kind, name, device_class, icon, unit in NODE_PRO_SENSORS
]
async_add_entities(sensors, True)
@ -117,53 +185,45 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
"""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."""
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_STATE: config_entry.data.get(CONF_STATE),
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._kind = kind
self._locale = locale
self._name = name
self._state = None
self._attr_icon = icon
self._attr_unit_of_measurement = unit
@property
def available(self):
"""Return True if entity is available."""
try:
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}"
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.coordinator.data["current"]["pollution"]
@callback
def update_from_latest_data(self):
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""
try:
data = self.coordinator.data["current"]["pollution"]
@ -172,17 +232,17 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
if self._kind == SENSOR_KIND_LEVEL:
aqi = data[f"aqi{self._locale}"]
[(self._state, self._attr_icon)] = [
[(self._attr_state, self._attr_icon)] = [
(name, icon)
for (floor, ceiling), (name, icon) in POLLUTANT_LEVELS.items()
if floor <= aqi <= ceiling
]
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:
symbol = data[f"main{self._locale}"]
self._state = POLLUTANT_LABELS[symbol]
self._attrs.update(
self._attr_state = symbol
self._attr_extra_state_attributes.update(
{
ATTR_POLLUTANT_SYMBOL: symbol,
ATTR_POLLUTANT_UNIT: POLLUTANT_UNITS[symbol],
@ -206,33 +266,43 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity):
)
if self._config_entry.options[CONF_SHOW_ON_MAP]:
self._attrs[ATTR_LATITUDE] = latitude
self._attrs[ATTR_LONGITUDE] = longitude
self._attrs.pop("lati", None)
self._attrs.pop("long", None)
self._attr_extra_state_attributes[ATTR_LATITUDE] = latitude
self._attr_extra_state_attributes[ATTR_LONGITUDE] = longitude
self._attr_extra_state_attributes.pop("lati", None)
self._attr_extra_state_attributes.pop("long", None)
else:
self._attrs["lati"] = latitude
self._attrs["long"] = longitude
self._attrs.pop(ATTR_LATITUDE, None)
self._attrs.pop(ATTR_LONGITUDE, None)
self._attr_extra_state_attributes["lati"] = latitude
self._attr_extra_state_attributes["long"] = longitude
self._attr_extra_state_attributes.pop(ATTR_LATITUDE, None)
self._attr_extra_state_attributes.pop(ATTR_LONGITUDE, None)
class AirVisualNodeProSensor(AirVisualEntity, SensorEntity):
"""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."""
super().__init__(coordinator)
self._kind = kind
self._name = name
self._state = None
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._kind = kind
@property
def device_info(self):
def device_info(self) -> DeviceInfo:
"""Return device registry information for this entity."""
return {
"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
def update_from_latest_data(self):
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""
if self._kind == SENSOR_KIND_BATTERY_LEVEL:
self._state = self.coordinator.data["status"]["battery"]
if self._kind == SENSOR_KIND_AQI:
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:
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:
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": {
"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"
},
"error": {
@ -35,18 +35,18 @@
"ip_address": "Host",
"password": "Passwort"
},
"description": "\u00dcberwachen Sie eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.",
"title": "Konfigurieren Sie einen AirVisual Node/Pro"
"description": "\u00dcberwache eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.",
"title": "Konfiguriere einen AirVisual Node/Pro"
},
"reauth_confirm": {
"data": {
"api_key": "API-Key"
"api_key": "API-Schl\u00fcssel"
},
"title": "AirVisual erneut authentifizieren"
},
"user": {
"description": "W\u00e4hlen Sie aus, welche Art von AirVisual-Daten Sie \u00fcberwachen m\u00f6chten.",
"title": "Konfigurieren Sie AirVisual"
"description": "W\u00e4hle aus, welche Art von AirVisual-Daten du \u00fcberwachen m\u00f6chtest.",
"title": "Konfiguriere AirVisual"
}
}
},
@ -54,9 +54,9 @@
"step": {
"init": {
"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": {
"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",
"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"
},
"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": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
"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": {
"geography_by_coords": {
@ -15,14 +16,19 @@
"api_key": "API kulcs",
"latitude": "Sz\u00e9less\u00e9g",
"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": {
"data": {
"api_key": "API kulcs",
"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": {
"data": {
@ -33,7 +39,8 @@
"reauth_confirm": {
"data": {
"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",
"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"
},
"geography_by_name": {
@ -25,9 +25,9 @@
"api_key": "\u041a\u043b\u044e\u0447 API",
"city": "\u0413\u043e\u0440\u043e\u0434",
"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"
},
"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