Merge pull request #38785 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2020-08-12 12:52:23 +02:00 committed by GitHub
commit 8eb6f29c53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
913 changed files with 26464 additions and 5921 deletions

View File

@ -8,6 +8,10 @@ omit =
homeassistant/scripts/*.py homeassistant/scripts/*.py
# omit pieces of code that rely on external devices being present # omit pieces of code that rely on external devices being present
homeassistant/components/accuweather/__init__.py
homeassistant/components/accuweather/const.py
homeassistant/components/accuweather/sensor.py
homeassistant/components/accuweather/weather.py
homeassistant/components/acer_projector/switch.py homeassistant/components/acer_projector/switch.py
homeassistant/components/actiontec/device_tracker.py homeassistant/components/actiontec/device_tracker.py
homeassistant/components/acmeda/__init__.py homeassistant/components/acmeda/__init__.py
@ -28,10 +32,6 @@ omit =
homeassistant/components/agent_dvr/camera.py homeassistant/components/agent_dvr/camera.py
homeassistant/components/agent_dvr/const.py homeassistant/components/agent_dvr/const.py
homeassistant/components/agent_dvr/helpers.py homeassistant/components/agent_dvr/helpers.py
homeassistant/components/airly/__init__.py
homeassistant/components/airly/air_quality.py
homeassistant/components/airly/sensor.py
homeassistant/components/airly/const.py
homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/__init__.py
homeassistant/components/airvisual/air_quality.py homeassistant/components/airvisual/air_quality.py
homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual/sensor.py
@ -69,6 +69,9 @@ omit =
homeassistant/components/avion/light.py homeassistant/components/avion/light.py
homeassistant/components/avri/const.py homeassistant/components/avri/const.py
homeassistant/components/avri/sensor.py homeassistant/components/avri/sensor.py
homeassistant/components/azure_devops/__init__.py
homeassistant/components/azure_devops/const.py
homeassistant/components/azure_devops/sensor.py
homeassistant/components/azure_service_bus/* homeassistant/components/azure_service_bus/*
homeassistant/components/baidu/tts.py homeassistant/components/baidu/tts.py
homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/beewi_smartclim/sensor.py
@ -139,6 +142,10 @@ omit =
homeassistant/components/comfoconnect/* homeassistant/components/comfoconnect/*
homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/alarm_control_panel.py
homeassistant/components/concord232/binary_sensor.py homeassistant/components/concord232/binary_sensor.py
homeassistant/components/control4/__init__.py
homeassistant/components/control4/light.py
homeassistant/components/control4/const.py
homeassistant/components/control4/director_utils.py
homeassistant/components/coolmaster/__init__.py homeassistant/components/coolmaster/__init__.py
homeassistant/components/coolmaster/climate.py homeassistant/components/coolmaster/climate.py
homeassistant/components/coolmaster/const.py homeassistant/components/coolmaster/const.py
@ -164,6 +171,8 @@ omit =
homeassistant/components/devolo_home_control/binary_sensor.py homeassistant/components/devolo_home_control/binary_sensor.py
homeassistant/components/devolo_home_control/const.py homeassistant/components/devolo_home_control/const.py
homeassistant/components/devolo_home_control/devolo_device.py homeassistant/components/devolo_home_control/devolo_device.py
homeassistant/components/devolo_home_control/devolo_multi_level_switch.py
homeassistant/components/devolo_home_control/light.py
homeassistant/components/devolo_home_control/sensor.py homeassistant/components/devolo_home_control/sensor.py
homeassistant/components/devolo_home_control/subscriber.py homeassistant/components/devolo_home_control/subscriber.py
homeassistant/components/devolo_home_control/switch.py homeassistant/components/devolo_home_control/switch.py
@ -254,6 +263,13 @@ omit =
homeassistant/components/fibaro/* homeassistant/components/fibaro/*
homeassistant/components/filesize/sensor.py homeassistant/components/filesize/sensor.py
homeassistant/components/fints/sensor.py homeassistant/components/fints/sensor.py
homeassistant/components/firmata/__init__.py
homeassistant/components/firmata/binary_sensor.py
homeassistant/components/firmata/board.py
homeassistant/components/firmata/const.py
homeassistant/components/firmata/entity.py
homeassistant/components/firmata/pin.py
homeassistant/components/firmata/switch.py
homeassistant/components/fitbit/sensor.py homeassistant/components/fitbit/sensor.py
homeassistant/components/fixer/sensor.py homeassistant/components/fixer/sensor.py
homeassistant/components/fleetgo/device_tracker.py homeassistant/components/fleetgo/device_tracker.py
@ -337,7 +353,8 @@ omit =
homeassistant/components/hisense_aehw4a1/* homeassistant/components/hisense_aehw4a1/*
homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hitron_coda/device_tracker.py
homeassistant/components/hive/* homeassistant/components/hive/*
homeassistant/components/hlk_sw16/* homeassistant/components/hlk_sw16/__init__.py
homeassistant/components/hlk_sw16/switch.py
homeassistant/components/home_connect/* homeassistant/components/home_connect/*
homeassistant/components/homematic/* homeassistant/components/homematic/*
homeassistant/components/homematic/climate.py homeassistant/components/homematic/climate.py
@ -443,8 +460,6 @@ omit =
homeassistant/components/lightwave/* homeassistant/components/lightwave/*
homeassistant/components/limitlessled/light.py homeassistant/components/limitlessled/light.py
homeassistant/components/linksys_smart/device_tracker.py homeassistant/components/linksys_smart/device_tracker.py
homeassistant/components/linky/__init__.py
homeassistant/components/linky/sensor.py
homeassistant/components/linode/* homeassistant/components/linode/*
homeassistant/components/linux_battery/sensor.py homeassistant/components/linux_battery/sensor.py
homeassistant/components/lirc/* homeassistant/components/lirc/*
@ -537,6 +552,10 @@ omit =
homeassistant/components/netatmo/camera.py homeassistant/components/netatmo/camera.py
homeassistant/components/netatmo/climate.py homeassistant/components/netatmo/climate.py
homeassistant/components/netatmo/const.py homeassistant/components/netatmo/const.py
homeassistant/components/netatmo/data_handler.py
homeassistant/components/netatmo/helper.py
homeassistant/components/netatmo/light.py
homeassistant/components/netatmo/netatmo_entity_base.py
homeassistant/components/netatmo/sensor.py homeassistant/components/netatmo/sensor.py
homeassistant/components/netatmo/webhook.py homeassistant/components/netatmo/webhook.py
homeassistant/components/netdata/sensor.py homeassistant/components/netdata/sensor.py
@ -605,6 +624,9 @@ omit =
homeassistant/components/orvibo/switch.py homeassistant/components/orvibo/switch.py
homeassistant/components/osramlightify/light.py homeassistant/components/osramlightify/light.py
homeassistant/components/otp/sensor.py homeassistant/components/otp/sensor.py
homeassistant/components/ovo_energy/__init__.py
homeassistant/components/ovo_energy/const.py
homeassistant/components/ovo_energy/sensor.py
homeassistant/components/panasonic_bluray/media_player.py homeassistant/components/panasonic_bluray/media_player.py
homeassistant/components/panasonic_viera/media_player.py homeassistant/components/panasonic_viera/media_player.py
homeassistant/components/pandora/media_player.py homeassistant/components/pandora/media_player.py
@ -617,6 +639,7 @@ omit =
homeassistant/components/picotts/tts.py homeassistant/components/picotts/tts.py
homeassistant/components/piglow/light.py homeassistant/components/piglow/light.py
homeassistant/components/pilight/* homeassistant/components/pilight/*
homeassistant/components/ping/const.py
homeassistant/components/ping/binary_sensor.py homeassistant/components/ping/binary_sensor.py
homeassistant/components/ping/device_tracker.py homeassistant/components/ping/device_tracker.py
homeassistant/components/pioneer/media_player.py homeassistant/components/pioneer/media_player.py
@ -679,7 +702,6 @@ omit =
homeassistant/components/rest/binary_sensor.py homeassistant/components/rest/binary_sensor.py
homeassistant/components/rest/notify.py homeassistant/components/rest/notify.py
homeassistant/components/rest/switch.py homeassistant/components/rest/switch.py
homeassistant/components/rfxtrx/*
homeassistant/components/ring/camera.py homeassistant/components/ring/camera.py
homeassistant/components/ripple/sensor.py homeassistant/components/ripple/sensor.py
homeassistant/components/rocketchat/notify.py homeassistant/components/rocketchat/notify.py
@ -729,7 +751,7 @@ omit =
homeassistant/components/simplisafe/lock.py homeassistant/components/simplisafe/lock.py
homeassistant/components/simulated/sensor.py homeassistant/components/simulated/sensor.py
homeassistant/components/sisyphus/* homeassistant/components/sisyphus/*
homeassistant/components/sky_hub/device_tracker.py homeassistant/components/sky_hub/*
homeassistant/components/skybeacon/sensor.py homeassistant/components/skybeacon/sensor.py
homeassistant/components/skybell/* homeassistant/components/skybell/*
homeassistant/components/slack/notify.py homeassistant/components/slack/notify.py
@ -909,6 +931,7 @@ omit =
homeassistant/components/vlc/media_player.py homeassistant/components/vlc/media_player.py
homeassistant/components/vlc_telnet/media_player.py homeassistant/components/vlc_telnet/media_player.py
homeassistant/components/volkszaehler/sensor.py homeassistant/components/volkszaehler/sensor.py
homeassistant/components/volumio/__init__.py
homeassistant/components/volumio/media_player.py homeassistant/components/volumio/media_player.py
homeassistant/components/volvooncall/* homeassistant/components/volvooncall/*
homeassistant/components/w800rf32/* homeassistant/components/w800rf32/*
@ -923,6 +946,9 @@ omit =
homeassistant/components/wiffi/* homeassistant/components/wiffi/*
homeassistant/components/wink/* homeassistant/components/wink/*
homeassistant/components/wirelesstag/* homeassistant/components/wirelesstag/*
homeassistant/components/wolflink/__init__.py
homeassistant/components/wolflink/sensor.py
homeassistant/components/wolflink/const.py
homeassistant/components/worldtidesinfo/sensor.py homeassistant/components/worldtidesinfo/sensor.py
homeassistant/components/worxlandroid/sensor.py homeassistant/components/worxlandroid/sensor.py
homeassistant/components/x10/light.py homeassistant/components/x10/light.py
@ -955,7 +981,6 @@ omit =
homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yale_smart_alarm/alarm_control_panel.py
homeassistant/components/yamaha_musiccast/media_player.py homeassistant/components/yamaha_musiccast/media_player.py
homeassistant/components/yandex_transport/* homeassistant/components/yandex_transport/*
homeassistant/components/yeelight/*
homeassistant/components/yeelightsunflower/light.py homeassistant/components/yeelightsunflower/light.py
homeassistant/components/yi/camera.py homeassistant/components/yi/camera.py
homeassistant/components/zabbix/* homeassistant/components/zabbix/*

View File

@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v2 uses: actions/setup-python@v2.1.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
@ -75,7 +75,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -119,7 +119,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -163,7 +163,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -229,7 +229,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -276,7 +276,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -323,7 +323,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -367,7 +367,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -414,7 +414,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -469,7 +469,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -516,7 +516,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -548,7 +548,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v2 uses: actions/setup-python@v2.1.1
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -737,7 +737,7 @@ jobs:
-p no:sugar \ -p no:sugar \
tests tests
- name: Upload coverage artifact - name: Upload coverage artifact
uses: actions/upload-artifact@2.1.0 uses: actions/upload-artifact@v2.1.3
with: with:
name: coverage-${{ matrix.python-version }}-group${{ matrix.group }} name: coverage-${{ matrix.python-version }}-group${{ matrix.group }}
path: .coverage path: .coverage
@ -781,4 +781,4 @@ jobs:
coverage report --fail-under=94 coverage report --fail-under=94
coverage xml coverage xml
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v1.0.10 uses: codecov/codecov-action@v1.0.12

View File

@ -12,6 +12,7 @@ addons:
- libavfilter-dev - libavfilter-dev
sources: sources:
- sourceline: ppa:savoury1/ffmpeg4 - sourceline: ppa:savoury1/ffmpeg4
- sourceline: ppa:savoury1/multimedia
python: python:
- "3.7.1" - "3.7.1"

View File

@ -14,6 +14,7 @@ homeassistant/scripts/check_config.py @kellerza
# Integrations # Integrations
homeassistant/components/abode/* @shred86 homeassistant/components/abode/* @shred86
homeassistant/components/accuweather/* @bieniu
homeassistant/components/acmeda/* @atmurray homeassistant/components/acmeda/* @atmurray
homeassistant/components/adguard/* @frenck homeassistant/components/adguard/* @frenck
homeassistant/components/agent_dvr/* @ispysoftware homeassistant/components/agent_dvr/* @ispysoftware
@ -48,6 +49,7 @@ homeassistant/components/avri/* @timvancann
homeassistant/components/awair/* @ahayworth @danielsjf homeassistant/components/awair/* @ahayworth @danielsjf
homeassistant/components/aws/* @awarecan homeassistant/components/aws/* @awarecan
homeassistant/components/axis/* @Kane610 homeassistant/components/axis/* @Kane610
homeassistant/components/azure_devops/* @timmo001
homeassistant/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/azure_event_hub/* @eavanvalkenburg
homeassistant/components/azure_service_bus/* @hfurubotten homeassistant/components/azure_service_bus/* @hfurubotten
homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/beewi_smartclim/* @alemuro
@ -77,6 +79,7 @@ homeassistant/components/cloudflare/* @ludeeus
homeassistant/components/comfoconnect/* @michaelarnauts homeassistant/components/comfoconnect/* @michaelarnauts
homeassistant/components/config/* @home-assistant/core homeassistant/components/config/* @home-assistant/core
homeassistant/components/configurator/* @home-assistant/core homeassistant/components/configurator/* @home-assistant/core
homeassistant/components/control4/* @lawtancool
homeassistant/components/conversation/* @home-assistant/core homeassistant/components/conversation/* @home-assistant/core
homeassistant/components/coolmaster/* @OnFreund homeassistant/components/coolmaster/* @OnFreund
homeassistant/components/coronavirus/* @home_assistant/core homeassistant/components/coronavirus/* @home_assistant/core
@ -110,7 +113,7 @@ homeassistant/components/edl21/* @mtdcr
homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/egardia/* @jeroenterheerdt
homeassistant/components/eight_sleep/* @mezz64 homeassistant/components/eight_sleep/* @mezz64
homeassistant/components/elgato/* @frenck homeassistant/components/elgato/* @frenck
homeassistant/components/elkm1/* @bdraco homeassistant/components/elkm1/* @gwww @bdraco
homeassistant/components/elv/* @majuss homeassistant/components/elv/* @majuss
homeassistant/components/emby/* @mezz64 homeassistant/components/emby/* @mezz64
homeassistant/components/emoncms/* @borpin homeassistant/components/emoncms/* @borpin
@ -128,6 +131,7 @@ homeassistant/components/ezviz/* @baqs
homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/fastdotcom/* @rohankapoorcom
homeassistant/components/file/* @fabaff homeassistant/components/file/* @fabaff
homeassistant/components/filter/* @dgomes homeassistant/components/filter/* @dgomes
homeassistant/components/firmata/* @DaAwesomeP
homeassistant/components/fixer/* @fabaff homeassistant/components/fixer/* @fabaff
homeassistant/components/flick_electric/* @ZephireNZ homeassistant/components/flick_electric/* @ZephireNZ
homeassistant/components/flock/* @fabaff homeassistant/components/flock/* @fabaff
@ -168,6 +172,7 @@ homeassistant/components/hikvisioncam/* @fbradyirl
homeassistant/components/hisense_aehw4a1/* @bannhead homeassistant/components/hisense_aehw4a1/* @bannhead
homeassistant/components/history/* @home-assistant/core homeassistant/components/history/* @home-assistant/core
homeassistant/components/hive/* @Rendili @KJonline homeassistant/components/hive/* @Rendili @KJonline
homeassistant/components/hlk_sw16/* @jameshilliard
homeassistant/components/home_connect/* @DavidMStraub homeassistant/components/home_connect/* @DavidMStraub
homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homeassistant/* @home-assistant/core
homeassistant/components/homekit/* @bdraco homeassistant/components/homekit/* @bdraco
@ -221,7 +226,6 @@ homeassistant/components/lametric/* @robbiet480
homeassistant/components/launch_library/* @ludeeus homeassistant/components/launch_library/* @ludeeus
homeassistant/components/lcn/* @alengwenus homeassistant/components/lcn/* @alengwenus
homeassistant/components/life360/* @pnbruckner homeassistant/components/life360/* @pnbruckner
homeassistant/components/linky/* @Quentame
homeassistant/components/linux_battery/* @fabaff homeassistant/components/linux_battery/* @fabaff
homeassistant/components/local_ip/* @issacg homeassistant/components/local_ip/* @issacg
homeassistant/components/logger/* @home-assistant/core homeassistant/components/logger/* @home-assistant/core
@ -239,7 +243,7 @@ homeassistant/components/mediaroom/* @dgomes
homeassistant/components/melcloud/* @vilppuvuorinen homeassistant/components/melcloud/* @vilppuvuorinen
homeassistant/components/melissa/* @kennedyshead homeassistant/components/melissa/* @kennedyshead
homeassistant/components/met/* @danielhiversen homeassistant/components/met/* @danielhiversen
homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame
homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/meteoalarm/* @rolfberkenbosch
homeassistant/components/metoffice/* @MrHarcombe homeassistant/components/metoffice/* @MrHarcombe
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
@ -297,6 +301,7 @@ homeassistant/components/openweathermap/* @fabaff
homeassistant/components/opnsense/* @mtreinish homeassistant/components/opnsense/* @mtreinish
homeassistant/components/orangepi_gpio/* @pascallj homeassistant/components/orangepi_gpio/* @pascallj
homeassistant/components/oru/* @bvlaicu homeassistant/components/oru/* @bvlaicu
homeassistant/components/ovo_energy/* @timmo001
homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare
homeassistant/components/panasonic_viera/* @joogps homeassistant/components/panasonic_viera/* @joogps
homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_custom/* @home-assistant/frontend
@ -362,6 +367,7 @@ homeassistant/components/signal_messenger/* @bbernhard
homeassistant/components/simplisafe/* @bachya homeassistant/components/simplisafe/* @bachya
homeassistant/components/sinch/* @bendikrb homeassistant/components/sinch/* @bendikrb
homeassistant/components/sisyphus/* @jkeljo homeassistant/components/sisyphus/* @jkeljo
homeassistant/components/sky_hub/* @rogerselwyn
homeassistant/components/slide/* @ualex73 homeassistant/components/slide/* @ualex73
homeassistant/components/sma/* @kellerza homeassistant/components/sma/* @kellerza
homeassistant/components/smappee/* @bsmappee homeassistant/components/smappee/* @bsmappee
@ -449,6 +455,7 @@ homeassistant/components/vilfo/* @ManneW
homeassistant/components/vivotek/* @HarlemSquirrel homeassistant/components/vivotek/* @HarlemSquirrel
homeassistant/components/vizio/* @raman325 homeassistant/components/vizio/* @raman325
homeassistant/components/vlc_telnet/* @rodripf homeassistant/components/vlc_telnet/* @rodripf
homeassistant/components/volumio/* @OnFreund
homeassistant/components/waqi/* @andrey-git homeassistant/components/waqi/* @andrey-git
homeassistant/components/watson_tts/* @rutkai homeassistant/components/watson_tts/* @rutkai
homeassistant/components/weather/* @fabaff homeassistant/components/weather/* @fabaff
@ -457,16 +464,17 @@ homeassistant/components/websocket_api/* @home-assistant/core
homeassistant/components/wiffi/* @mampfes homeassistant/components/wiffi/* @mampfes
homeassistant/components/withings/* @vangorra homeassistant/components/withings/* @vangorra
homeassistant/components/wled/* @frenck homeassistant/components/wled/* @frenck
homeassistant/components/wolflink/* @adamkrol93
homeassistant/components/workday/* @fabaff homeassistant/components/workday/* @fabaff
homeassistant/components/worldclock/* @fabaff homeassistant/components/worldclock/* @fabaff
homeassistant/components/xbox_live/* @MartinHjelmare homeassistant/components/xbox_live/* @MartinHjelmare
homeassistant/components/xfinity/* @cisasteelersfan homeassistant/components/xfinity/* @cisasteelersfan
homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi
homeassistant/components/xiaomi_miio/* @rytilahti @syssi homeassistant/components/xiaomi_miio/* @rytilahti @syssi @starkillerOG
homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xiaomi_tv/* @simse
homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/xmpp/* @fabaff @flowolf
homeassistant/components/yamaha_musiccast/* @jalmeroth homeassistant/components/yamaha_musiccast/* @jalmeroth
homeassistant/components/yandex_transport/* @rishatik92 homeassistant/components/yandex_transport/* @rishatik92 @devbis
homeassistant/components/yeelight/* @rytilahti @zewelor homeassistant/components/yeelight/* @rytilahti @zewelor
homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yeelightsunflower/* @lindsaymarkward
homeassistant/components/yessssms/* @flowolf homeassistant/components/yessssms/* @flowolf

View File

@ -29,12 +29,31 @@ jobs:
- template: templates/azp-job-wheels.yaml@azure - template: templates/azp-job-wheels.yaml@azure
parameters: parameters:
builderVersion: '$(versionWheels)' builderVersion: '$(versionWheels)'
builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev'
builderPip: 'Cython;numpy' builderPip: 'Cython;numpy'
skipBinary: 'aiohttp' skipBinary: 'aiohttp'
wheelsRequirement: 'requirements.txt'
wheelsRequirementDiff: 'requirements_diff.txt'
wheelsConstraint: 'homeassistant/package_constraints.txt'
jobName: 'Wheels_Core'
preBuild:
- script: |
if [[ "$(Build.Reason)" =~ (Schedule|Manual) ]]; then
exit 0
else
curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/core/master/requirements.txt
fi
displayName: 'Prepare requirements files for Home Assistant Core wheels'
- template: templates/azp-job-wheels.yaml@azure
parameters:
builderVersion: '$(versionWheels)'
builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev'
builderPip: 'Cython;numpy;scikit-build'
skipBinary: 'aiohttp'
wheelsRequirement: 'requirements_wheels.txt' wheelsRequirement: 'requirements_wheels.txt'
wheelsRequirementDiff: 'requirements_diff.txt' wheelsRequirementDiff: 'requirements_diff.txt'
wheelsConstraint: 'homeassistant/package_constraints.txt' wheelsConstraint: 'homeassistant/package_constraints.txt'
jobName: 'Wheels_Integrations'
preBuild: preBuild:
- script: | - script: |
cp requirements_all.txt requirements_wheels.txt cp requirements_all.txt requirements_wheels.txt

View File

@ -1,11 +1,11 @@
{ {
"image": "homeassistant/{arch}-homeassistant", "image": "homeassistant/{arch}-homeassistant",
"build_from": { "build_from": {
"aarch64": "homeassistant/aarch64-homeassistant-base:8.0.0", "aarch64": "homeassistant/aarch64-homeassistant-base:8.2.1",
"armhf": "homeassistant/armhf-homeassistant-base:8.0.0", "armhf": "homeassistant/armhf-homeassistant-base:8.2.1",
"armv7": "homeassistant/armv7-homeassistant-base:8.0.0", "armv7": "homeassistant/armv7-homeassistant-base:8.2.1",
"amd64": "homeassistant/amd64-homeassistant-base:8.0.0", "amd64": "homeassistant/amd64-homeassistant-base:8.2.1",
"i386": "homeassistant/i386-homeassistant-base:8.0.0" "i386": "homeassistant/i386-homeassistant-base:8.2.1"
}, },
"labels": { "labels": {
"io.hass.type": "core" "io.hass.type": "core"

View File

@ -7,7 +7,7 @@ from homeassistant.util.async_ import protect_loop
def enable() -> None: def enable() -> None:
"""Enable the detection of I/O in the event loop.""" """Enable the detection of I/O in the event loop."""
# Prevent urllib3 and requests doing I/O in event loop # Prevent urllib3 and requests doing I/O in event loop
HTTPConnection.putrequest = protect_loop(HTTPConnection.putrequest) HTTPConnection.putrequest = protect_loop(HTTPConnection.putrequest) # type: ignore
# Currently disabled. pytz doing I/O when getting timezone. # Currently disabled. pytz doing I/O when getting timezone.
# Prevent files being opened inside the event loop # Prevent files being opened inside the event loop

View File

@ -6,10 +6,10 @@ import logging
import logging.handlers import logging.handlers
import os import os
import sys import sys
import threading
from time import monotonic from time import monotonic
from typing import TYPE_CHECKING, Any, Dict, Optional, Set from typing import TYPE_CHECKING, Any, Dict, Optional, Set
from async_timeout import timeout
import voluptuous as vol import voluptuous as vol
import yarl import yarl
@ -44,6 +44,11 @@ DATA_LOGGING = "logging"
LOG_SLOW_STARTUP_INTERVAL = 60 LOG_SLOW_STARTUP_INTERVAL = 60
STAGE_1_TIMEOUT = 120
STAGE_2_TIMEOUT = 300
WRAP_UP_TIMEOUT = 300
COOLDOWN_TIME = 60
DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"} DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"}
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
LOGGING_INTEGRATIONS = { LOGGING_INTEGRATIONS = {
@ -136,7 +141,7 @@ async def async_setup_hass(
hass.async_track_tasks() hass.async_track_tasks()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
with contextlib.suppress(asyncio.TimeoutError): with contextlib.suppress(asyncio.TimeoutError):
async with timeout(10): async with hass.timeout.async_timeout(10):
await hass.async_block_till_done() await hass.async_block_till_done()
safe_mode = True safe_mode = True
@ -304,6 +309,12 @@ def async_enable_logging(
"Uncaught exception", exc_info=args # type: ignore "Uncaught exception", exc_info=args # type: ignore
) )
if sys.version_info[:2] >= (3, 8):
threading.excepthook = lambda args: logging.getLogger(None).exception(
"Uncaught thread exception",
exc_info=(args.exc_type, args.exc_value, args.exc_traceback),
)
# Log errors to a file if we have write access to file or config dir # Log errors to a file if we have write access to file or config dir
if log_file is None: if log_file is None:
err_log_path = hass.config.path(ERROR_LOG_FILENAME) err_log_path = hass.config.path(ERROR_LOG_FILENAME)
@ -496,24 +507,42 @@ async def _async_set_up_integrations(
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
# Kick off loading the registries. They don't need to be awaited. # Kick off loading the registries. They don't need to be awaited.
asyncio.gather( asyncio.create_task(hass.helpers.device_registry.async_get_registry())
hass.helpers.device_registry.async_get_registry(), asyncio.create_task(hass.helpers.entity_registry.async_get_registry())
hass.helpers.entity_registry.async_get_registry(), asyncio.create_task(hass.helpers.area_registry.async_get_registry())
hass.helpers.area_registry.async_get_registry(),
)
# Start setup # Start setup
if stage_1_domains: if stage_1_domains:
_LOGGER.info("Setting up stage 1: %s", stage_1_domains) _LOGGER.info("Setting up stage 1: %s", stage_1_domains)
await async_setup_multi_components(hass, stage_1_domains, config, setup_started) try:
async with hass.timeout.async_timeout(
STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME
):
await async_setup_multi_components(
hass, stage_1_domains, config, setup_started
)
except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for stage 1 - moving forward")
# Enables after dependencies # Enables after dependencies
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains) async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
if stage_2_domains: if stage_2_domains:
_LOGGER.info("Setting up stage 2: %s", stage_2_domains) _LOGGER.info("Setting up stage 2: %s", stage_2_domains)
await async_setup_multi_components(hass, stage_2_domains, config, setup_started) try:
async with hass.timeout.async_timeout(
STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME
):
await async_setup_multi_components(
hass, stage_2_domains, config, setup_started
)
except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for stage 2 - moving forward")
# Wrap up startup # Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up") _LOGGER.debug("Waiting for startup to wrap up")
try:
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
await hass.async_block_till_done() await hass.async_block_till_done()
except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for bootstrap - moving forward")

View File

@ -0,0 +1,132 @@
"""The AccuWeather component."""
import asyncio
from datetime import timedelta
import logging
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
from homeassistant.const import CONF_API_KEY
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
ATTR_FORECAST,
CONF_FORECAST,
COORDINATOR,
DOMAIN,
UNDO_UPDATE_LISTENER,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor", "weather"]
async def async_setup(hass: HomeAssistant, config: Config) -> bool:
"""Set up configured AccuWeather."""
hass.data.setdefault(DOMAIN, {})
return True
async def async_setup_entry(hass, config_entry) -> bool:
"""Set up AccuWeather as config entry."""
api_key = config_entry.data[CONF_API_KEY]
location_key = config_entry.unique_id
forecast = config_entry.options.get(CONF_FORECAST, False)
_LOGGER.debug("Using location_key: %s, get forecast: %s", location_key, forecast)
websession = async_get_clientsession(hass)
coordinator = AccuWeatherDataUpdateCoordinator(
hass, websession, api_key, location_key, forecast
)
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
undo_listener = config_entry.add_update_listener(update_listener)
hass.data[DOMAIN][config_entry.entry_id] = {
COORDINATOR: coordinator,
UNDO_UPDATE_LISTENER: undo_listener,
}
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component)
)
return True
async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(config_entry, component)
for component in PLATFORMS
]
)
)
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
async def update_listener(hass, config_entry):
"""Update listener."""
await hass.config_entries.async_reload(config_entry.entry_id)
class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching AccuWeather data API."""
def __init__(self, hass, session, api_key, location_key, forecast: bool):
"""Initialize."""
self.location_key = location_key
self.forecast = forecast
self.is_metric = hass.config.units.is_metric
self.accuweather = AccuWeather(api_key, session, location_key=self.location_key)
# Enabling the forecast download increases the number of requests per data
# update, we use 32 minutes for current condition only and 64 minutes for
# current condition and forecast as update interval to not exceed allowed number
# of requests. We have 50 requests allowed per day, so we use 45 and leave 5 as
# a reserve for restarting HA.
update_interval = (
timedelta(minutes=64) if self.forecast else timedelta(minutes=32)
)
_LOGGER.debug("Data will be update every %s", update_interval)
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
async def _async_update_data(self):
"""Update data via library."""
try:
async with timeout(10):
current = await self.accuweather.async_get_current_conditions()
forecast = (
await self.accuweather.async_get_forecast(metric=self.is_metric)
if self.forecast
else {}
)
except (
ApiError,
ClientConnectorError,
InvalidApiKeyError,
RequestsExceededError,
) as error:
raise UpdateFailed(error)
_LOGGER.debug("Requests remaining: %s", self.accuweather.requests_remaining)
return {**current, **{ATTR_FORECAST: forecast}}

View File

@ -0,0 +1,112 @@
"""Adds config flow for AccuWeather."""
import asyncio
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientError
from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from .const import CONF_FORECAST, DOMAIN # pylint:disable=unused-import
class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for AccuWeather."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
# Under the terms of use of the API, one user can use one free API key. Due to
# the small number of requests allowed, we only allow one integration instance.
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
errors = {}
if user_input is not None:
websession = async_get_clientsession(self.hass)
try:
with timeout(10):
accuweather = AccuWeather(
user_input[CONF_API_KEY],
websession,
latitude=user_input[CONF_LATITUDE],
longitude=user_input[CONF_LONGITUDE],
)
await accuweather.async_get_location()
except (ApiError, ClientConnectorError, asyncio.TimeoutError, ClientError):
errors["base"] = "cannot_connect"
except InvalidApiKeyError:
errors[CONF_API_KEY] = "invalid_api_key"
except RequestsExceededError:
errors[CONF_API_KEY] = "requests_exceeded"
else:
await self.async_set_unique_id(
accuweather.location_key, raise_on_progress=False
)
return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_API_KEY): str,
vol.Optional(
CONF_LATITUDE, default=self.hass.config.latitude
): cv.latitude,
vol.Optional(
CONF_LONGITUDE, default=self.hass.config.longitude
): cv.longitude,
vol.Optional(
CONF_NAME, default=self.hass.config.location_name
): str,
}
),
errors=errors,
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Options callback for AccuWeather."""
return AccuWeatherOptionsFlowHandler(config_entry)
class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow):
"""Config flow options for AccuWeather."""
def __init__(self, config_entry):
"""Initialize AccuWeather options flow."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
"""Manage the options."""
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Optional(
CONF_FORECAST,
default=self.config_entry.options.get(CONF_FORECAST, False),
): bool
}
),
)

View File

@ -0,0 +1,279 @@
"""Constants for AccuWeather integration."""
from homeassistant.const import (
ATTR_DEVICE_CLASS,
DEVICE_CLASS_TEMPERATURE,
LENGTH_FEET,
LENGTH_INCHES,
LENGTH_METERS,
SPEED_KILOMETERS_PER_HOUR,
SPEED_MILES_PER_HOUR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
TIME_HOURS,
UNIT_PERCENTAGE,
UV_INDEX,
VOLUME_CUBIC_METERS,
)
ATTRIBUTION = "Data provided by AccuWeather"
ATTR_ICON = "icon"
ATTR_FORECAST = CONF_FORECAST = "forecast"
ATTR_LABEL = "label"
ATTR_UNIT_IMPERIAL = "Imperial"
ATTR_UNIT_METRIC = "Metric"
CONCENTRATION_PARTS_PER_CUBIC_METER = f"p/{VOLUME_CUBIC_METERS}"
COORDINATOR = "coordinator"
DOMAIN = "accuweather"
LENGTH_MILIMETERS = "mm"
MANUFACTURER = "AccuWeather, Inc."
NAME = "AccuWeather"
UNDO_UPDATE_LISTENER = "undo_update_listener"
CONDITION_CLASSES = {
"clear-night": [33, 34, 37],
"cloudy": [7, 8, 38],
"exceptional": [24, 30, 31],
"fog": [11],
"hail": [25],
"lightning": [15],
"lightning-rainy": [16, 17, 41, 42],
"partlycloudy": [4, 6, 35, 36],
"pouring": [18],
"rainy": [12, 13, 14, 26, 39, 40],
"snowy": [19, 20, 21, 22, 23, 43, 44],
"snowy-rainy": [29],
"sunny": [1, 2, 3, 5],
"windy": [32],
}
FORECAST_DAYS = [0, 1, 2, 3, 4]
FORECAST_SENSOR_TYPES = {
"CloudCoverDay": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-cloudy",
ATTR_LABEL: "Cloud Cover Day",
ATTR_UNIT_METRIC: UNIT_PERCENTAGE,
ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE,
},
"CloudCoverNight": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-cloudy",
ATTR_LABEL: "Cloud Cover Night",
ATTR_UNIT_METRIC: UNIT_PERCENTAGE,
ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE,
},
"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,
},
"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,
},
"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,
},
"Ozone": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:vector-triangle",
ATTR_LABEL: "Ozone",
ATTR_UNIT_METRIC: None,
ATTR_UNIT_IMPERIAL: None,
},
"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,
},
"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,
},
"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,
},
"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,
},
"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,
},
"ThunderstormProbabilityDay": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-lightning",
ATTR_LABEL: "Thunderstorm Probability Day",
ATTR_UNIT_METRIC: UNIT_PERCENTAGE,
ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE,
},
"ThunderstormProbabilityNight": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-lightning",
ATTR_LABEL: "Thunderstorm Probability Night",
ATTR_UNIT_METRIC: UNIT_PERCENTAGE,
ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE,
},
"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,
},
"UVIndex": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-sunny",
ATTR_LABEL: "UV Index",
ATTR_UNIT_METRIC: UV_INDEX,
ATTR_UNIT_IMPERIAL: UV_INDEX,
},
"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,
},
"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,
},
}
OPTIONAL_SENSORS = (
"ApparentTemperature",
"CloudCover",
"CloudCoverDay",
"CloudCoverNight",
"DewPoint",
"Grass",
"Mold",
"Ozone",
"Ragweed",
"RealFeelTemperatureShade",
"RealFeelTemperatureShadeMax",
"RealFeelTemperatureShadeMin",
"Tree",
"WetBulbTemperature",
"WindChillTemperature",
"WindGust",
"WindGustDay",
"WindGustNight",
)
SENSOR_TYPES = {
"ApparentTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "Apparent Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
},
"Ceiling": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-fog",
ATTR_LABEL: "Cloud Ceiling",
ATTR_UNIT_METRIC: LENGTH_METERS,
ATTR_UNIT_IMPERIAL: LENGTH_FEET,
},
"CloudCover": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-cloudy",
ATTR_LABEL: "Cloud Cover",
ATTR_UNIT_METRIC: UNIT_PERCENTAGE,
ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE,
},
"DewPoint": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "Dew Point",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
},
"RealFeelTemperature": {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_LABEL: "RealFeel Temperature",
ATTR_UNIT_METRIC: TEMP_CELSIUS,
ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT,
},
"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,
},
"Precipitation": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-rainy",
ATTR_LABEL: "Precipitation",
ATTR_UNIT_METRIC: LENGTH_MILIMETERS,
ATTR_UNIT_IMPERIAL: LENGTH_INCHES,
},
"PressureTendency": {
ATTR_DEVICE_CLASS: "accuweather__pressure_tendency",
ATTR_ICON: "mdi:gauge",
ATTR_LABEL: "Pressure Tendency",
ATTR_UNIT_METRIC: None,
ATTR_UNIT_IMPERIAL: None,
},
"UVIndex": {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:weather-sunny",
ATTR_LABEL: "UV Index",
ATTR_UNIT_METRIC: UV_INDEX,
ATTR_UNIT_IMPERIAL: UV_INDEX,
},
"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,
},
"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,
},
"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,
},
}

View File

@ -0,0 +1,8 @@
{
"domain": "accuweather",
"name": "AccuWeather",
"documentation": "https://www.home-assistant.io/integrations/accuweather/",
"requirements": ["accuweather==0.0.9"],
"codeowners": ["@bieniu"],
"config_flow": true
}

View File

@ -0,0 +1,189 @@
"""Support for the AccuWeather service."""
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
CONF_NAME,
DEVICE_CLASS_TEMPERATURE,
)
from homeassistant.helpers.entity import Entity
from .const import (
ATTR_FORECAST,
ATTR_ICON,
ATTR_LABEL,
ATTRIBUTION,
COORDINATOR,
DOMAIN,
FORECAST_DAYS,
FORECAST_SENSOR_TYPES,
MANUFACTURER,
NAME,
OPTIONAL_SENSORS,
SENSOR_TYPES,
)
PARALLEL_UPDATES = 1
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add AccuWeather entities from a config_entry."""
name = config_entry.data[CONF_NAME]
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
sensors = []
for sensor in SENSOR_TYPES:
sensors.append(AccuWeatherSensor(name, sensor, coordinator))
if coordinator.forecast:
for sensor in FORECAST_SENSOR_TYPES:
for day in FORECAST_DAYS:
# Some air quality/allergy sensors are only available for certain
# locations.
if sensor in coordinator.data[ATTR_FORECAST][0]:
sensors.append(
AccuWeatherSensor(name, sensor, coordinator, forecast_day=day)
)
async_add_entities(sensors, False)
class AccuWeatherSensor(Entity):
"""Define an AccuWeather entity."""
def __init__(self, name, kind, coordinator, forecast_day=None):
"""Initialize."""
self._name = name
self.kind = kind
self.coordinator = coordinator
self._device_class = None
self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
self.forecast_day = forecast_day
@property
def name(self):
"""Return the name."""
if self.forecast_day is not None:
return f"{self._name} {FORECAST_SENSOR_TYPES[self.kind][ATTR_LABEL]} {self.forecast_day}d"
return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}"
@property
def unique_id(self):
"""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):
"""Return the device info."""
return {
"identifiers": {(DOMAIN, self.coordinator.location_key)},
"name": NAME,
"manufacturer": MANUFACTURER,
"entry_type": "service",
}
@property
def should_poll(self):
"""Return the polling requirement of the entity."""
return False
@property
def available(self):
"""Return True if entity is available."""
return self.coordinator.last_update_success
@property
def state(self):
"""Return the state."""
if self.forecast_day is not None:
if (
FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
== DEVICE_CLASS_TEMPERATURE
):
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][
self.kind
]["Value"]
if self.kind in ["WindGustDay", "WindGustNight"]:
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][
self.kind
]["Speed"]["Value"]
if self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]:
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][
self.kind
]["Value"]
return self.coordinator.data[ATTR_FORECAST][self.forecast_day][self.kind]
if self.kind == "Ceiling":
return round(self.coordinator.data[self.kind][self._unit_system]["Value"])
if self.kind == "PressureTendency":
return self.coordinator.data[self.kind]["LocalizedText"].lower()
if SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE:
return self.coordinator.data[self.kind][self._unit_system]["Value"]
if self.kind == "Precipitation":
return self.coordinator.data["PrecipitationSummary"][self.kind][
self._unit_system
]["Value"]
if self.kind == "WindGust":
return self.coordinator.data[self.kind]["Speed"][self._unit_system]["Value"]
return self.coordinator.data[self.kind]
@property
def icon(self):
"""Return the icon."""
if self.forecast_day is not None:
return FORECAST_SENSOR_TYPES[self.kind][ATTR_ICON]
return SENSOR_TYPES[self.kind][ATTR_ICON]
@property
def device_class(self):
"""Return the device_class."""
if self.forecast_day is not None:
return FORECAST_SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS]
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
if self.forecast_day is not None:
return FORECAST_SENSOR_TYPES[self.kind][self._unit_system]
return SENSOR_TYPES[self.kind][self._unit_system]
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self.forecast_day is not None:
if self.kind == "WindGustDay":
self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][
self.forecast_day
][self.kind]["Direction"]["English"]
elif self.kind == "WindGustNight":
self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][
self.forecast_day
][self.kind]["Direction"]["English"]
elif self.kind in ["Grass", "Mold", "Ragweed", "Tree", "UVIndex", "Ozone"]:
self._attrs["level"] = self.coordinator.data[ATTR_FORECAST][
self.forecast_day
][self.kind]["Category"]
return self._attrs
if self.kind == "UVIndex":
self._attrs["level"] = self.coordinator.data["UVIndexText"]
elif self.kind == "Precipitation":
self._attrs["type"] = self.coordinator.data["PrecipitationType"]
return self._attrs
@property
def entity_registry_enabled_default(self):
"""Return if the entity should be enabled when first added to the entity registry."""
return bool(self.kind not in OPTIONAL_SENSORS)
async def async_added_to_hass(self):
"""Connect to dispatcher listening for entity data notifications."""
self.async_on_remove(
self.coordinator.async_add_listener(self.async_write_ha_state)
)
async def async_update(self):
"""Update AccuWeather entity."""
await self.coordinator.async_request_refresh()

View File

@ -0,0 +1,35 @@
{
"config": {
"step": {
"user": {
"title": "AccuWeather",
"description": "If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/accuweather/\n\nSome sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options.",
"data": {
"name": "Name of the integration",
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "Latitude",
"longitude": "Longitude"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key."
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"options": {
"step": {
"user": {
"title": "AccuWeather Options",
"description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 64 minutes instead of every 32 minutes.",
"data": {
"forecast": "Weather forecast"
}
}
}
}
}

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"steady": "Steady",
"rising": "Rising",
"falling": "Falling"
}
}
}

View File

@ -0,0 +1,35 @@
{
"config": {
"abort": {
"single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
},
"error": {
"cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_api_key": "Clau API inv\u00e0lida",
"requests_exceeded": "S'ha superat el nombre m\u00e0xim de sol\u00b7licituds permeses a l'API d'AccuWeather. Has d'esperar-te o canviar la clau API."
},
"step": {
"user": {
"data": {
"api_key": "Clau API",
"latitude": "Latitud",
"longitude": "Longitud",
"name": "Nom de la integraci\u00f3"
},
"description": "Si necessites ajuda amb la configuraci\u00f3, consulta: https://www.home-assistant.io/integrations/accuweather/ \n\n La previsi\u00f3 meteorol\u00f2gica no est\u00e0 habilitada de manera predeterminada. Pots activar-la en les opcions de la integraci\u00f3.",
"title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "Previsi\u00f3 meteorol\u00f2gica"
},
"description": "Per culpa de les limitacions de la versi\u00f3 gratu\u00efta l'API d'AccuWeather, quan habilitis la previsi\u00f3 meteorol\u00f2gica, les actualitzacions es realitzaran cada 64 minuts en comptes de 32.",
"title": "Opcions d'AccuWeather"
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"config": {
"abort": {
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_api_key": "Invalid API key",
"requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key."
},
"step": {
"user": {
"data": {
"api_key": "API Key",
"latitude": "Latitude",
"longitude": "Longitude",
"name": "Name of the integration"
},
"description": "If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/accuweather/\n\nSome sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options.",
"title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "Weather forecast"
},
"description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 64 minutes instead of every 32 minutes.",
"title": "AccuWeather Options"
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"config": {
"abort": {
"single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n."
},
"error": {
"cannot_connect": "No se pudo conectar",
"invalid_api_key": "Clave API no v\u00e1lida",
"requests_exceeded": "Se ha excedido el n\u00famero permitido de solicitudes a la API de Accuweather. Tienes que esperar o cambiar la Clave API."
},
"step": {
"user": {
"data": {
"api_key": "Clave API",
"latitude": "Latitud",
"longitude": "Longitud",
"name": "Nombre de la integraci\u00f3n"
},
"description": "Si necesitas ayuda con la configuraci\u00f3n, echa un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/accuweather/ \n\nEl pron\u00f3stico del tiempo no est\u00e1 habilitado por defecto. Puedes habilitarlo en las opciones de la integraci\u00f3n.",
"title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "Pron\u00f3stico del tiempo"
},
"description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos.",
"title": "Opciones de AccuWeather"
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"config": {
"abort": {
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"error": {
"cannot_connect": "Impossibile connettersi",
"invalid_api_key": "Chiave API non valida",
"requests_exceeded": "\u00c8 stato superato il numero consentito di richieste all'API di Accuweather. \u00c8 necessario attendere o modificare la chiave API."
},
"step": {
"user": {
"data": {
"api_key": "Chiave API",
"latitude": "Latitudine",
"longitude": "Logitudine",
"name": "Nome dell'integrazione"
},
"description": "Se hai bisogno di aiuto con la configurazione dai un'occhiata qui: https://www.home-assistant.io/integrations/accuweather/ \n\nAlcuni sensori non sono abilitati per impostazione predefinita. \u00c8 possibile abilitarli nel registro entit\u00e0 dopo la configurazione di integrazione. \nLe previsioni meteo non sono abilitate per impostazione predefinita. Puoi abilitarle nelle opzioni di integrazione.",
"title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "Previsioni meteo"
},
"description": "A causa delle limitazioni della versione gratuita della chiave API AccuWeather, quando si abilitano le previsioni del tempo, gli aggiornamenti dei dati verranno eseguiti ogni 64 minuti invece che ogni 32 minuti.",
"title": "Opzioni AccuWeather"
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"config": {
"abort": {
"single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
},
"error": {
"cannot_connect": "Feeler beim verbannen",
"invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel",
"requests_exceeded": "D\u00e9i zougelooss Zuel vun Ufroen un Accuweather API gouf iwwerschratt. Du muss ofwaarden oder den API Schl\u00ebssel \u00e4nneren."
},
"step": {
"user": {
"data": {
"api_key": "API Schl\u00ebssel",
"latitude": "Breedegrad",
"longitude": "L\u00e4ngegrad",
"name": "Numm vun der Integratioun"
},
"description": "Falls du H\u00ebllef mat der Konfiguratioun brauch kuck h\u00e9i:\nhttps://www.home-assistant.io/integrations/accuweather/\n\nWieder Pr\u00e9visounen si standardm\u00e9isseg net aktiv. Du kanns d\u00e9i an den Optioune vun der Integratioun aschalten.",
"title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "Wieder Pr\u00e9visioun"
},
"description": "Duerch d'Limite vun der Gratis Versioun vun der AccuWeather API, wann d'Wieder Pr\u00e9visoune aktiv\u00e9iert sinn, ginn d'Aktualis\u00e9ierungen all 64 Minutten gemaach, am plaatz vun all 32 Minutten.",
"title": "AccuWeather Optiounen"
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"config": {
"abort": {
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"error": {
"cannot_connect": "Tilkobling mislyktes.",
"invalid_api_key": "Ugyldig API-n\u00f8kkel",
"requests_exceeded": "Det tillatte antallet foresp\u00f8rsler til Accuweather API er overskredet. Du m\u00e5 vente eller endre API-n\u00f8kkel."
},
"step": {
"user": {
"data": {
"api_key": "API-n\u00f8kkel",
"latitude": "Breddegrad",
"longitude": "Lengdegrad",
"name": "Navn p\u00e5 integrasjon"
},
"description": "Hvis du trenger hjelp med konfigurasjonen, kan du se her: https://www.home-assistant.io/integrations/accuweather/ \n\n Noen sensorer er ikke aktivert som standard. Du kan aktivere dem i enhetsregisteret etter integrasjonskonfigurasjonen. \n V\u00e6rmelding er ikke aktivert som standard. Du kan aktivere det i integrasjonsalternativene.",
"title": ""
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "V\u00e6rmelding"
},
"description": "P\u00e5 grunn av begrensningene i gratisversjonen av AccuWeather API-n\u00f8kkelen, n\u00e5r du aktiverer v\u00e6rmelding, vil dataoppdateringer bli utf\u00f8rt hvert 64. minutt i stedet for hvert 32. minutt.",
"title": "AccuWeather-alternativer"
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"config": {
"abort": {
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
"invalid_api_key": "Nieprawid\u0142owy klucz API.",
"requests_exceeded": "Dozwolona liczba zapyta\u0144 do interfejsu API Accuweather zosta\u0142a przekroczona. Musisz poczeka\u0107 lub zmieni\u0107 klucz API."
},
"step": {
"user": {
"data": {
"api_key": "Klucz API",
"latitude": "Szeroko\u015b\u0107 geograficzna",
"longitude": "D\u0142ugo\u015b\u0107 geograficzna",
"name": "Nazwa integracji"
},
"description": "Je\u015bli potrzebujesz pomocy z konfiguracj\u0105, przejd\u017a na stron\u0119: https://www.home-assistant.io/integrations/accuweather/ \n\nCz\u0119\u015b\u0107 sensor\u00f3w nie jest w\u0142\u0105czona domy\u015blnie. Mo\u017cesz je w\u0142\u0105czy\u0107 w rejestrze encji po konfiguracji integracji.\nPrognoza pogody nie jest domy\u015blnie w\u0142\u0105czona. Mo\u017cesz j\u0105 w\u0142\u0105czy\u0107 w opcjach integracji.",
"title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "Prognoza pogody"
},
"description": "Ze wzgl\u0119du na ograniczenia darmowej wersji klucza API AccuWeather po w\u0142\u0105czeniu prognozy pogody aktualizacje danych b\u0119d\u0105 wykonywane co 64 minut zamiast co 32 minut.",
"title": "Opcje AccuWeather"
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"step": {
"user": {
"data": {
"latitude": "Latitude",
"longitude": "Longitude"
}
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "Previs\u00e3o meteorol\u00f3gica"
}
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"config": {
"abort": {
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
},
"error": {
"cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.",
"invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.",
"requests_exceeded": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043a API Accuweather. \u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u043e\u0434\u043e\u0436\u0434\u0430\u0442\u044c \u0438\u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API."
},
"step": {
"user": {
"data": {
"api_key": "\u041a\u043b\u044e\u0447 API",
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
"longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430",
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435"
},
"description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438, \u0435\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u0430 \u043f\u043e\u043c\u043e\u0449\u044c \u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439:\nhttps://www.home-assistant.io/integrations/accuweather/ \n\n\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b \u0441\u043a\u0440\u044b\u0442\u044b \u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u043d\u0443\u0436\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0432 \u0440\u0435\u0435\u0441\u0442\u0440\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0438 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.",
"title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b"
},
"description": "\u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0435 64 \u043c\u0438\u043d\u0443\u0442\u044b, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 32 \u043c\u0438\u043d\u0443\u0442\u044b.",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AccuWeather"
}
}
}
}

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "Klesaj\u00edc\u00ed",
"rising": "Roustouc\u00ed",
"steady": "St\u00e1l\u00fd"
}
}
}

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "Falling",
"rising": "Rising",
"steady": "Steady"
}
}
}

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "Cayendo",
"rising": "Subiendo",
"steady": "Estable"
}
}
}

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "Diminuzione",
"rising": "Aumento",
"steady": "Stabile"
}
}
}

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "Fallende",
"rising": "Stiger",
"steady": "Jevn"
}
}
}

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "spada",
"rising": "ro\u015bnie",
"steady": "bez zmian"
}
}
}

View File

@ -0,0 +1,9 @@
{
"state": {
"accuweather__pressure_tendency": {
"falling": "\u041f\u043e\u043d\u0438\u0436\u0430\u044e\u0449\u0435\u0435\u0441\u044f",
"rising": "\u041f\u043e\u0432\u044b\u0448\u0430\u044e\u0449\u0435\u0435\u0441\u044f",
"steady": "\u041f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e\u0435"
}
}
}

View File

@ -0,0 +1,26 @@
{
"config": {
"error": {
"invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API"
},
"step": {
"user": {
"data": {
"latitude": "\u0428\u0438\u0440\u043e\u0442\u0430",
"longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430",
"name": "\u041d\u0430\u0437\u0432\u0430 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457"
},
"title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438"
}
}
}
}
}

View File

@ -0,0 +1,35 @@
{
"config": {
"abort": {
"single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_api_key": "API \u5bc6\u9470\u7121\u6548",
"requests_exceeded": "\u5df2\u8d85\u904e Accuweather API \u5141\u8a31\u7684\u8acb\u6c42\u6b21\u6578\u3002\u5fc5\u9808\u7b49\u5019\u6216\u8b8a\u66f4 API \u5bc6\u9470\u3002"
},
"step": {
"user": {
"data": {
"api_key": "API \u5bc6\u9470",
"latitude": "\u7def\u5ea6",
"longitude": "\u7d93\u5ea6",
"name": "\u6574\u5408\u540d\u7a31"
},
"description": "\u5047\u5982\u4f60\u9700\u8981\u5354\u52a9\u9032\u884c\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/accuweather/\n\n\u5929\u6c23\u9810\u5831\u9810\u8a2d\u672a\u958b\u555f\u3002\u53ef\u4ee5\u65bc\u6574\u5408\u9078\u9805\u4e2d\u958b\u555f\u3002",
"title": "AccuWeather"
}
}
},
"options": {
"step": {
"user": {
"data": {
"forecast": "\u5929\u6c23\u9810\u5831"
},
"description": "\u7531\u65bc AccuWeather API \u5bc6\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 64 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 32 \u5206\u9418\u3002",
"title": "AccuWeather \u9078\u9805"
}
}
}
}

View File

@ -0,0 +1,195 @@
"""Support for the AccuWeather service."""
from statistics import mean
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
WeatherEntity,
)
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.util.dt import utc_from_timestamp
from .const import (
ATTR_FORECAST,
ATTRIBUTION,
CONDITION_CLASSES,
COORDINATOR,
DOMAIN,
MANUFACTURER,
NAME,
)
PARALLEL_UPDATES = 1
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add a AccuWeather weather entity from a config_entry."""
name = config_entry.data[CONF_NAME]
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
async_add_entities([AccuWeatherEntity(name, coordinator)], False)
class AccuWeatherEntity(WeatherEntity):
"""Define an AccuWeather entity."""
def __init__(self, name, coordinator):
"""Initialize."""
self._name = name
self.coordinator = coordinator
self._attrs = {}
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
@property
def name(self):
"""Return the name."""
return self._name
@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION
@property
def unique_id(self):
"""Return a unique_id for this entity."""
return self.coordinator.location_key
@property
def device_info(self):
"""Return the device info."""
return {
"identifiers": {(DOMAIN, self.coordinator.location_key)},
"name": NAME,
"manufacturer": MANUFACTURER,
"entry_type": "service",
}
@property
def should_poll(self):
"""Return the polling requirement of the entity."""
return False
@property
def available(self):
"""Return True if entity is available."""
return self.coordinator.last_update_success
@property
def condition(self):
"""Return the current condition."""
try:
return [
k
for k, v in CONDITION_CLASSES.items()
if self.coordinator.data["WeatherIcon"] in v
][0]
except IndexError:
return None
@property
def temperature(self):
"""Return the temperature."""
return self.coordinator.data["Temperature"][self._unit_system]["Value"]
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT
@property
def pressure(self):
"""Return the pressure."""
return self.coordinator.data["Pressure"][self._unit_system]["Value"]
@property
def humidity(self):
"""Return the humidity."""
return self.coordinator.data["RelativeHumidity"]
@property
def wind_speed(self):
"""Return the wind speed."""
return self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"]
@property
def wind_bearing(self):
"""Return the wind bearing."""
return self.coordinator.data["Wind"]["Direction"]["Degrees"]
@property
def visibility(self):
"""Return the visibility."""
return self.coordinator.data["Visibility"][self._unit_system]["Value"]
@property
def ozone(self):
"""Return the ozone level."""
# We only have ozone data for certain locations and only in the forecast data.
if self.coordinator.forecast and self.coordinator.data[ATTR_FORECAST][0].get(
"Ozone"
):
return self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"]
return None
@property
def forecast(self):
"""Return the forecast array."""
if not self.coordinator.forecast:
return None
# remap keys from library to keys understood by the weather component
forecast = [
{
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
ATTR_FORECAST_TEMP: item["TemperatureMax"]["Value"],
ATTR_FORECAST_TEMP_LOW: item["TemperatureMin"]["Value"],
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(item),
ATTR_FORECAST_PRECIPITATION_PROBABILITY: round(
mean(
[
item["PrecipitationProbabilityDay"],
item["PrecipitationProbabilityNight"],
]
)
),
ATTR_FORECAST_WIND_SPEED: item["WindDay"]["Speed"]["Value"],
ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"],
ATTR_FORECAST_CONDITION: [
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v
][0],
}
for item in self.coordinator.data[ATTR_FORECAST]
]
return forecast
async def async_added_to_hass(self):
"""Connect to dispatcher listening for entity data notifications."""
self.async_on_remove(
self.coordinator.async_add_listener(self.async_write_ha_state)
)
async def async_update(self):
"""Update AccuWeather entity."""
await self.coordinator.async_request_refresh()
@staticmethod
def _calc_precipitation(day: dict) -> float:
"""Return sum of the precipitation."""
precip_sum = 0
precip_types = ["Rain", "Snow", "Ice"]
for precip in precip_types:
precip_sum = sum(
[
precip_sum,
day[f"{precip}Day"]["Value"],
day[f"{precip}Night"]["Value"],
]
)
return round(precip_sum, 1)

View File

@ -17,8 +17,10 @@
"user": { "user": {
"data": { "data": {
"host": "Vert", "host": "Vert",
"password": "Passord",
"port": "", "port": "",
"ssl": "AdGuard Hjem bruker et SSL-sertifikat", "ssl": "AdGuard Hjem bruker et SSL-sertifikat",
"username": "Brukernavn",
"verify_ssl": "AdGuard Home bruker et riktig sertifikat" "verify_ssl": "AdGuard Home bruker et riktig sertifikat"
}, },
"description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll." "description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll."

View File

@ -3,7 +3,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"username": "E-pasts" "port": "Poort"
} }
} }
} }

View File

@ -11,7 +11,7 @@
"user": { "user": {
"data": { "data": {
"host": "Vert", "host": "Vert",
"port": "Port" "port": ""
}, },
"title": "Konfigurere Agent DVR" "title": "Konfigurere Agent DVR"
} }

View File

@ -18,7 +18,9 @@ from .const import (
ATTR_API_PM25, ATTR_API_PM25,
ATTR_API_PM25_LIMIT, ATTR_API_PM25_LIMIT,
ATTR_API_PM25_PERCENT, ATTR_API_PM25_PERCENT,
DEFAULT_NAME,
DOMAIN, DOMAIN,
MANUFACTURER,
) )
ATTRIBUTION = "Data provided by Airly" ATTRIBUTION = "Data provided by Airly"
@ -31,6 +33,8 @@ LABEL_PM_2_5_PERCENT = f"{ATTR_PM_2_5}_percent_of_limit"
LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit" LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit"
LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit" LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit"
PARALLEL_UPDATES = 1
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Airly air_quality entity based on a config entry.""" """Set up Airly air_quality entity based on a config entry."""
@ -38,9 +42,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
coordinator = hass.data[DOMAIN][config_entry.entry_id] coordinator = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities( async_add_entities([AirlyAirQuality(coordinator, name)], False)
[AirlyAirQuality(coordinator, name, config_entry.unique_id)], False
)
def round_state(func): def round_state(func):
@ -58,11 +60,10 @@ def round_state(func):
class AirlyAirQuality(AirQualityEntity): class AirlyAirQuality(AirQualityEntity):
"""Define an Airly air quality.""" """Define an Airly air quality."""
def __init__(self, coordinator, name, unique_id): def __init__(self, coordinator, name):
"""Initialize.""" """Initialize."""
self.coordinator = coordinator self.coordinator = coordinator
self._name = name self._name = name
self._unique_id = unique_id
self._icon = "mdi:blur" self._icon = "mdi:blur"
@property @property
@ -106,7 +107,19 @@ class AirlyAirQuality(AirQualityEntity):
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique_id for this entity.""" """Return a unique_id for this entity."""
return self._unique_id return f"{self.coordinator.latitude}-{self.coordinator.longitude}"
@property
def device_info(self):
"""Return the device info."""
return {
"identifiers": {
(DOMAIN, self.coordinator.latitude, self.coordinator.longitude)
},
"name": DEFAULT_NAME,
"manufacturer": MANUFACTURER,
"entry_type": "service",
}
@property @property
def available(self): def available(self):

View File

@ -15,5 +15,6 @@ ATTR_API_PRESSURE = "PRESSURE"
ATTR_API_TEMPERATURE = "TEMPERATURE" ATTR_API_TEMPERATURE = "TEMPERATURE"
DEFAULT_NAME = "Airly" DEFAULT_NAME = "Airly"
DOMAIN = "airly" DOMAIN = "airly"
MANUFACTURER = "Airly sp. z o.o."
MAX_REQUESTS_PER_DAY = 100 MAX_REQUESTS_PER_DAY = 100
NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet." NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet."

View File

@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/airly", "documentation": "https://www.home-assistant.io/integrations/airly",
"codeowners": ["@bieniu"], "codeowners": ["@bieniu"],
"requirements": ["airly==0.0.2"], "requirements": ["airly==0.0.2"],
"config_flow": true "config_flow": true,
"quality_scale": "platinum"
} }

View File

@ -18,7 +18,9 @@ from .const import (
ATTR_API_PM1, ATTR_API_PM1,
ATTR_API_PRESSURE, ATTR_API_PRESSURE,
ATTR_API_TEMPERATURE, ATTR_API_TEMPERATURE,
DEFAULT_NAME,
DOMAIN, DOMAIN,
MANUFACTURER,
) )
ATTRIBUTION = "Data provided by Airly" ATTRIBUTION = "Data provided by Airly"
@ -27,6 +29,8 @@ ATTR_ICON = "icon"
ATTR_LABEL = "label" ATTR_LABEL = "label"
ATTR_UNIT = "unit" ATTR_UNIT = "unit"
PARALLEL_UPDATES = 1
SENSOR_TYPES = { SENSOR_TYPES = {
ATTR_API_PM1: { ATTR_API_PM1: {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
@ -63,8 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
sensors = [] sensors = []
for sensor in SENSOR_TYPES: for sensor in SENSOR_TYPES:
unique_id = f"{config_entry.unique_id}-{sensor.lower()}" sensors.append(AirlySensor(coordinator, name, sensor))
sensors.append(AirlySensor(coordinator, name, sensor, unique_id))
async_add_entities(sensors, False) async_add_entities(sensors, False)
@ -72,11 +75,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class AirlySensor(Entity): class AirlySensor(Entity):
"""Define an Airly sensor.""" """Define an Airly sensor."""
def __init__(self, coordinator, name, kind, unique_id): def __init__(self, coordinator, name, kind):
"""Initialize.""" """Initialize."""
self.coordinator = coordinator self.coordinator = coordinator
self._name = name self._name = name
self._unique_id = unique_id
self.kind = kind self.kind = kind
self._device_class = None self._device_class = None
self._state = None self._state = None
@ -123,7 +125,19 @@ class AirlySensor(Entity):
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique_id for this entity.""" """Return a unique_id for this entity."""
return self._unique_id return f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}"
@property
def device_info(self):
"""Return the device info."""
return {
"identifiers": {
(DOMAIN, self.coordinator.latitude, self.coordinator.longitude)
},
"name": DEFAULT_NAME,
"manufacturer": MANUFACTURER,
"entry_type": "service",
}
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):

View File

@ -10,7 +10,7 @@
"step": { "step": {
"user": { "user": {
"data": { "data": {
"api_key": "Clave API de Airly", "api_key": "Clave API",
"latitude": "Latitud", "latitude": "Latitud",
"longitude": "Longitud", "longitude": "Longitud",
"name": "Nombre de la integraci\u00f3n" "name": "Nombre de la integraci\u00f3n"

View File

@ -1,25 +1,39 @@
{ {
"device_automation": { "device_automation": {
"action_type": { "action_type": {
"arm_away": "Aktivovat {entity_name} v re\u017eimu mimo domov", "arm_away": "Aktivovat {entity_name} v re\u017eimu nep\u0159\u00edtomnost",
"arm_home": "Aktivovat {entity_name} v re\u017eimu doma", "arm_home": "Aktivovat {entity_name} v re\u017eimu domov",
"arm_night": "Aktivovat {entity_name} v re\u017eimu noc", "arm_night": "Aktivovat {entity_name} v no\u010dn\u00edm re\u017eimu",
"disarm": "Deaktivovat {entity_name}", "disarm": "Odbezpe\u010dit {entity_name}",
"trigger": "Spustit {entity_name}" "trigger": "Spustit {entity_name}"
},
"condition_type": {
"is_armed_away": "{entity_name} je v re\u017eimu nep\u0159\u00edtomnost",
"is_armed_home": "{entity_name} je v re\u017eimu domov",
"is_armed_night": "{entity_name} je v no\u010dn\u00edm re\u017eimu",
"is_disarmed": "{entity_name} nen\u00ed zabezpe\u010den",
"is_triggered": "{entity_name} je spu\u0161t\u011bn"
},
"trigger_type": {
"armed_away": "{entity_name} v re\u017eimu nep\u0159\u00edtomnost",
"armed_home": "{entity_name} v re\u017eimu domov",
"armed_night": "{entity_name} v no\u010dn\u00edm re\u017eimu",
"disarmed": "{entity_name} nezabezpe\u010den",
"triggered": "{entity_name} spu\u0161t\u011bn"
} }
}, },
"state": { "state": {
"_": { "_": {
"armed": "Aktivn\u00ed", "armed": "Zabezpe\u010deno",
"armed_away": "Aktivn\u00ed re\u017eim mimo domov", "armed_away": "Re\u017eim nep\u0159\u00edtomnost",
"armed_custom_bypass": "Aktivn\u00ed u\u017eivatelsk\u00fdm obejit\u00edm", "armed_custom_bypass": "Zabezpe\u010deno u\u017eivatelsk\u00fdm obejit\u00edm",
"armed_home": "Aktivn\u00ed re\u017eim doma", "armed_home": "Re\u017eim domov",
"armed_night": "Aktivn\u00ed no\u010dn\u00ed re\u017eim", "armed_night": "No\u010dn\u00ed re\u017eim",
"arming": "Aktivov\u00e1n\u00ed", "arming": "Zabezpe\u010dov\u00e1n\u00ed",
"disarmed": "Neaktivn\u00ed", "disarmed": "Nezabezpe\u010deno",
"disarming": "Deaktivov\u00e1n\u00ed", "disarming": "Odbezpe\u010dov\u00e1n\u00ed",
"pending": "Nadch\u00e1zej\u00edc\u00ed", "pending": "\u010cekaj\u00edc\u00ed",
"triggered": "Spu\u0161t\u011bno" "triggered": "Spu\u0161t\u011bn"
} }
}, },
"title": "Ovl\u00e1dac\u00ed panel alarmu" "title": "Ovl\u00e1dac\u00ed panel alarmu"

View File

@ -84,6 +84,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
self._name = "Alarm Panel" self._name = "Alarm Panel"
self._state = None self._state = None
self._ac_power = None self._ac_power = None
self._alarm_event_occurred = None
self._backlight_on = None self._backlight_on = None
self._battery_low = None self._battery_low = None
self._check_zone = None self._check_zone = None
@ -117,6 +118,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
self._ac_power = message.ac_power self._ac_power = message.ac_power
self._alarm_event_occurred = message.alarm_event_occurred
self._backlight_on = message.backlight_on self._backlight_on = message.backlight_on
self._battery_low = message.battery_low self._battery_low = message.battery_low
self._check_zone = message.check_zone self._check_zone = message.check_zone
@ -163,6 +165,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
"""Return the state attributes.""" """Return the state attributes."""
return { return {
"ac_power": self._ac_power, "ac_power": self._ac_power,
"alarm_event_occurred": self._alarm_event_occurred,
"backlight_on": self._backlight_on, "backlight_on": self._backlight_on,
"battery_low": self._battery_low, "battery_low": self._battery_low,
"check_zone": self._check_zone, "check_zone": self._check_zone,

View File

@ -293,7 +293,7 @@ async def async_setup_entry(hass, config_entry):
Client( Client(
config_entry.data[CONF_API_KEY], config_entry.data[CONF_API_KEY],
config_entry.data[CONF_APP_KEY], config_entry.data[CONF_APP_KEY],
session, session=session,
), ),
) )
hass.loop.create_task(ambient.ws_connect()) hass.loop.create_task(ambient.ws_connect())

View File

@ -43,7 +43,9 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
session = aiohttp_client.async_get_clientsession(self.hass) session = aiohttp_client.async_get_clientsession(self.hass)
client = Client(user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session) client = Client(
user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session=session
)
try: try:
devices = await client.api.get_devices() devices = await client.api.get_devices()

View File

@ -3,6 +3,6 @@
"name": "Ambient Weather Station", "name": "Ambient Weather Station",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ambient_station", "documentation": "https://www.home-assistant.io/integrations/ambient_station",
"requirements": ["aioambient==1.1.1"], "requirements": ["aioambient==1.2.1"],
"codeowners": ["@bachya"] "codeowners": ["@bachya"]
} }

View File

@ -5,7 +5,6 @@ import logging
import os import os
from adb_shell.auth.keygen import keygen from adb_shell.auth.keygen import keygen
from adb_shell.auth.sign_pythonrsa import PythonRSASigner
from adb_shell.exceptions import ( from adb_shell.exceptions import (
AdbTimeoutError, AdbTimeoutError,
InvalidChecksumError, InvalidChecksumError,
@ -14,6 +13,7 @@ from adb_shell.exceptions import (
TcpTimeoutException, TcpTimeoutException,
) )
from androidtv import ha_state_detection_rules_validator from androidtv import ha_state_detection_rules_validator
from androidtv.adb_manager.adb_manager_sync import ADBPythonSync
from androidtv.constants import APPS, KEYS from androidtv.constants import APPS, KEYS
from androidtv.exceptions import LockNotAcquiredException from androidtv.exceptions import LockNotAcquiredException
from androidtv.setup_async import setup from androidtv.setup_async import setup
@ -40,6 +40,7 @@ from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_NAME, CONF_NAME,
CONF_PORT, CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
STATE_IDLE, STATE_IDLE,
STATE_OFF, STATE_OFF,
STATE_PAUSED, STATE_PAUSED,
@ -175,9 +176,7 @@ def setup_androidtv(hass, config):
keygen(adbkey) keygen(adbkey)
# Load the ADB key # Load the ADB key
with open(adbkey) as priv_key: signer = ADBPythonSync.load_adbkey(adbkey)
priv = priv_key.read()
signer = PythonRSASigner("", priv)
adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" adb_log = f"using Python ADB implementation with adbkey='{adbkey}'"
else: else:
@ -230,6 +229,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
) )
raise PlatformNotReady raise PlatformNotReady
async def _async_close(event):
"""Close the ADB socket connection when HA stops."""
await aftv.adb_close()
# Close the ADB connection when HA stops
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close)
device_args = [ device_args = [
aftv, aftv,
config[CONF_NAME], config[CONF_NAME],

View File

@ -11,6 +11,10 @@
"description": "Vil du legge Arcam FMJ p\u00e5 ` {host} ` til Home Assistant? " "description": "Vil du legge Arcam FMJ p\u00e5 ` {host} ` til Home Assistant? "
}, },
"user": { "user": {
"data": {
"host": "Vert",
"port": ""
},
"description": "Vennligst skriv inn vertsnavnet eller IP-adressen til enheten." "description": "Vennligst skriv inn vertsnavnet eller IP-adressen til enheten."
} }
} }

View File

@ -1,5 +1,8 @@
{ {
"config": { "config": {
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
},
"step": { "step": {
"user": { "user": {
"data": { "data": {

View File

@ -12,7 +12,7 @@
"data": { "data": {
"email": "E-post (valgfritt)", "email": "E-post (valgfritt)",
"host": "Vert", "host": "Vert",
"port": "Port " "port": ""
}, },
"title": "Koble til enheten" "title": "Koble til enheten"
} }

View File

@ -23,7 +23,13 @@ from homeassistant.const import (
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_ON, STATE_ON,
) )
from homeassistant.core import Context, CoreState, HomeAssistant, callback from homeassistant.core import (
Context,
CoreState,
HomeAssistant,
callback,
split_entity_id,
)
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import condition, extract_domain_configs from homeassistant.helpers import condition, extract_domain_configs
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -61,6 +67,7 @@ CONF_TRIGGER = "trigger"
CONF_CONDITION_TYPE = "condition_type" CONF_CONDITION_TYPE = "condition_type"
CONF_INITIAL_STATE = "initial_state" CONF_INITIAL_STATE = "initial_state"
CONF_SKIP_CONDITION = "skip_condition" CONF_SKIP_CONDITION = "skip_condition"
CONF_STOP_ACTIONS = "stop_actions"
CONDITION_USE_TRIGGER_VALUES = "use_trigger_values" CONDITION_USE_TRIGGER_VALUES = "use_trigger_values"
CONDITION_TYPE_AND = "and" CONDITION_TYPE_AND = "and"
@ -69,6 +76,7 @@ CONDITION_TYPE_OR = "or"
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
DEFAULT_INITIAL_STATE = True DEFAULT_INITIAL_STATE = True
DEFAULT_STOP_ACTIONS = True
EVENT_AUTOMATION_RELOADED = "automation_reloaded" EVENT_AUTOMATION_RELOADED = "automation_reloaded"
EVENT_AUTOMATION_TRIGGERED = "automation_triggered" EVENT_AUTOMATION_TRIGGERED = "automation_triggered"
@ -219,7 +227,11 @@ async def async_setup(hass, config):
) )
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on")
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") component.async_register_entity_service(
SERVICE_TURN_OFF,
{vol.Optional(CONF_STOP_ACTIONS, default=DEFAULT_STOP_ACTIONS): cv.boolean},
"async_turn_off",
)
async def reload_service_handler(service_call): async def reload_service_handler(service_call):
"""Remove all automations and load new ones from config.""" """Remove all automations and load new ones from config."""
@ -255,11 +267,13 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self._async_detach_triggers = None self._async_detach_triggers = None
self._cond_func = cond_func self._cond_func = cond_func
self.action_script = action_script self.action_script = action_script
self.action_script.change_listener = self.async_write_ha_state
self._last_triggered = None self._last_triggered = None
self._initial_state = initial_state self._initial_state = initial_state
self._is_enabled = False self._is_enabled = False
self._referenced_entities: Optional[Set[str]] = None self._referenced_entities: Optional[Set[str]] = None
self._referenced_devices: Optional[Set[str]] = None self._referenced_devices: Optional[Set[str]] = None
self._logger = _LOGGER
@property @property
def name(self): def name(self):
@ -282,11 +296,10 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
attrs = { attrs = {
ATTR_LAST_TRIGGERED: self._last_triggered, ATTR_LAST_TRIGGERED: self._last_triggered,
ATTR_MODE: self.action_script.script_mode, ATTR_MODE: self.action_script.script_mode,
ATTR_CUR: self.action_script.runs,
} }
if self.action_script.supports_max: if self.action_script.supports_max:
attrs[ATTR_MAX] = self.action_script.max_runs attrs[ATTR_MAX] = self.action_script.max_runs
if self.is_on:
attrs[ATTR_CUR] = self.action_script.runs
return attrs return attrs
@property @property
@ -337,13 +350,18 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
"""Startup with initial state or previous state.""" """Startup with initial state or previous state."""
await super().async_added_to_hass() await super().async_added_to_hass()
self._logger = logging.getLogger(
f"{__name__}.{split_entity_id(self.entity_id)[1]}"
)
self.action_script.update_logger(self._logger)
state = await self.async_get_last_state() state = await self.async_get_last_state()
if state: if state:
enable_automation = state.state == STATE_ON enable_automation = state.state == STATE_ON
last_triggered = state.attributes.get("last_triggered") last_triggered = state.attributes.get("last_triggered")
if last_triggered is not None: if last_triggered is not None:
self._last_triggered = parse_datetime(last_triggered) self._last_triggered = parse_datetime(last_triggered)
_LOGGER.debug( self._logger.debug(
"Loaded automation %s with state %s from state " "Loaded automation %s with state %s from state "
" storage last state %s", " storage last state %s",
self.entity_id, self.entity_id,
@ -352,7 +370,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
) )
else: else:
enable_automation = DEFAULT_INITIAL_STATE enable_automation = DEFAULT_INITIAL_STATE
_LOGGER.debug( self._logger.debug(
"Automation %s not in state storage, state %s from default is used", "Automation %s not in state storage, state %s from default is used",
self.entity_id, self.entity_id,
enable_automation, enable_automation,
@ -360,7 +378,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
if self._initial_state is not None: if self._initial_state is not None:
enable_automation = self._initial_state enable_automation = self._initial_state
_LOGGER.debug( self._logger.debug(
"Automation %s initial state %s overridden from " "Automation %s initial state %s overridden from "
"config initial_state", "config initial_state",
self.entity_id, self.entity_id,
@ -376,6 +394,9 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off.""" """Turn the entity off."""
if CONF_STOP_ACTIONS in kwargs:
await self.async_disable(kwargs[CONF_STOP_ACTIONS])
else:
await self.async_disable() await self.async_disable()
async def async_trigger(self, variables, skip_condition=False, context=None): async def async_trigger(self, variables, skip_condition=False, context=None):
@ -403,12 +424,12 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
context=trigger_context, context=trigger_context,
) )
_LOGGER.info("Executing %s", self._name) self._logger.info("Executing %s", self._name)
try: try:
await self.action_script.async_run(variables, trigger_context) await self.action_script.async_run(variables, trigger_context)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("While executing automation %s", self.entity_id) self._logger.exception("While executing automation %s", self.entity_id)
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
"""Remove listeners when removing automation from Home Assistant.""" """Remove listeners when removing automation from Home Assistant."""
@ -444,9 +465,9 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
) )
self.async_write_ha_state() self.async_write_ha_state()
async def async_disable(self): async def async_disable(self, stop_actions=DEFAULT_STOP_ACTIONS):
"""Disable the automation entity.""" """Disable the automation entity."""
if not self._is_enabled: if not self._is_enabled and not self.action_script.runs:
return return
self._is_enabled = False self._is_enabled = False
@ -455,6 +476,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self._async_detach_triggers() self._async_detach_triggers()
self._async_detach_triggers = None self._async_detach_triggers = None
if stop_actions:
await self.action_script.async_stop() await self.action_script.async_stop()
self.async_write_ha_state() self.async_write_ha_state()
@ -478,13 +500,13 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
results = await asyncio.gather(*triggers) results = await asyncio.gather(*triggers)
if None in results: if None in results:
_LOGGER.error("Error setting up trigger %s", self._name) self._logger.error("Error setting up trigger %s", self._name)
removes = [remove for remove in results if remove is not None] removes = [remove for remove in results if remove is not None]
if not removes: if not removes:
return None return None
_LOGGER.info("Initialized trigger %s", self._name) self._logger.info("Initialized trigger %s", self._name)
@callback @callback
def remove_triggers(): def remove_triggers():

View File

@ -12,6 +12,9 @@ turn_off:
entity_id: entity_id:
description: Name of the automation to turn off. description: Name of the automation to turn off.
example: "automation.notify_home" example: "automation.notify_home"
stop_actions:
description: Stop currently running actions (defaults to true).
example: false
toggle: toggle:
description: Toggle an automation. description: Toggle an automation.
@ -27,7 +30,7 @@ trigger:
description: Name of the automation to trigger. description: Name of the automation to trigger.
example: "automation.notify_home" example: "automation.notify_home"
skip_condition: skip_condition:
description: Whether or not the condition will be skipped (defaults to True). description: Whether or not the condition will be skipped (defaults to true).
example: true example: true
reload: reload:

View File

@ -73,16 +73,13 @@ async def async_attach_trigger(
from_s = event.data.get("old_state") from_s = event.data.get("old_state")
to_s = event.data.get("new_state") to_s = event.data.get("new_state")
old_state = getattr(from_s, "state", None)
new_state = getattr(to_s, "state", None)
if ( if (
(from_s is not None and not match_from_state(from_s.state)) not match_from_state(old_state)
or (to_s is not None and not match_to_state(to_s.state)) or not match_to_state(new_state)
or ( or (not match_all and old_state == new_state)
not match_all
and from_s is not None
and to_s is not None
and from_s.state == to_s.state
)
): ):
return return
@ -104,15 +101,6 @@ async def async_attach_trigger(
) )
) )
# Ignore changes to state attributes if from/to is in use
if (
not match_all
and from_s is not None
and to_s is not None
and from_s.state == to_s.state
):
return
if not time_delta: if not time_delta:
call_action() call_action()
return return

View File

@ -13,20 +13,37 @@ from homeassistant.helpers.event import async_track_time_change
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.Schema( TRIGGER_SCHEMA = vol.Schema(
{vol.Required(CONF_PLATFORM): "time", vol.Required(CONF_AT): cv.time} {
vol.Required(CONF_PLATFORM): "time",
vol.Required(CONF_AT): vol.All(cv.ensure_list, [cv.time]),
}
) )
async def async_attach_trigger(hass, config, action, automation_info): async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
at_time = config.get(CONF_AT) at_times = config[CONF_AT]
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
@callback @callback
def time_automation_listener(now): def time_automation_listener(now):
"""Listen for time changes and calls action.""" """Listen for time changes and calls action."""
hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}}) hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}})
return async_track_time_change( removes = [
hass, time_automation_listener, hour=hours, minute=minutes, second=seconds async_track_time_change(
hass,
time_automation_listener,
hour=at_time.hour,
minute=at_time.minute,
second=at_time.second,
) )
for at_time in at_times
]
@callback
def remove_track_time_changes():
"""Remove tracked time changes."""
for remove in removes:
remove()
return remove_track_time_changes

View File

@ -1,17 +1,25 @@
{ {
"config": { "config": {
"abort": {
"already_configured": "Kontoen er allerede konfigurert",
"no_devices": "Ingen enheter funnet p\u00e5 nettverket",
"reauth_successful": "Tilgangstoken oppdatert"
},
"error": { "error": {
"auth": "Ugyldig tilgangstoken",
"unknown": "Ukjent Awair API-feil." "unknown": "Ukjent Awair API-feil."
}, },
"step": { "step": {
"reauth": { "reauth": {
"data": { "data": {
"access_token": "Tilgangstoken",
"email": "Epost" "email": "Epost"
}, },
"description": "Skriv inn tilgangstokenet for Awair-utviklere p\u00e5 nytt." "description": "Skriv inn tilgangstokenet for Awair-utviklere p\u00e5 nytt."
}, },
"user": { "user": {
"data": { "data": {
"access_token": "Tilgangstoken",
"email": "Epost " "email": "Epost "
}, },
"description": "Du m\u00e5 registrere deg for et Awair-utviklertilgangstoken p\u00e5: https://developer.getawair.com/onboard/login" "description": "Du m\u00e5 registrere deg for et Awair-utviklertilgangstoken p\u00e5: https://developer.getawair.com/onboard/login"

View File

@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "Konto jest ju\u017c skonfigurowane.",
"no_devices": "Nie znaleziono urz\u0105dze\u0144 w sieci.",
"reauth_successful": "Token dost\u0119pu pomy\u015blnie zaktualizowano."
},
"error": {
"auth": "Token dost\u0119pu"
},
"step": {
"reauth": {
"data": {
"access_token": "Token dost\u0119pu",
"email": "Adres e-mail"
}
},
"user": {
"data": {
"access_token": "Token dost\u0119pu",
"email": "Adres e-mail"
}
}
}
}
}

View File

@ -18,7 +18,7 @@
"data": { "data": {
"host": "Vert", "host": "Vert",
"password": "Passord", "password": "Passord",
"port": "Port", "port": "",
"username": "Brukernavn" "username": "Brukernavn"
}, },
"title": "Sett opp Axis enhet" "title": "Sett opp Axis enhet"

View File

@ -0,0 +1,121 @@
"""Support for Azure DevOps."""
import logging
from typing import Any, Dict
from aioazuredevops.client import DevOpsClient
import aiohttp
from homeassistant.components.azure_devops.const import (
CONF_ORG,
CONF_PAT,
CONF_PROJECT,
DATA_AZURE_DEVOPS_CLIENT,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Set up the Azure DevOps components."""
return True
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up Azure DevOps from a config entry."""
client = DevOpsClient()
try:
if entry.data[CONF_PAT] is not None:
await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG])
if not client.authorized:
_LOGGER.warning(
"Could not authorize with Azure DevOps. You may need to update your token"
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": "reauth"}, data=entry.data,
)
)
return False
await client.get_project(entry.data[CONF_ORG], entry.data[CONF_PROJECT])
except aiohttp.ClientError as exception:
_LOGGER.warning(exception)
raise ConfigEntryNotReady from exception
instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"
hass.data.setdefault(instance_key, {})[DATA_AZURE_DEVOPS_CLIENT] = client
# Setup components
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
return True
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool:
"""Unload Azure DevOps config entry."""
del hass.data[f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"]
return await hass.config_entries.async_forward_entry_unload(entry, "sensor")
class AzureDevOpsEntity(Entity):
"""Defines a base Azure DevOps entity."""
def __init__(self, organization: str, project: str, name: str, icon: str) -> None:
"""Initialize the Azure DevOps entity."""
self._name = name
self._icon = icon
self._available = True
self.organization = organization
self.project = project
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name
@property
def icon(self) -> str:
"""Return the mdi icon of the entity."""
return self._icon
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available
async def async_update(self) -> None:
"""Update Azure DevOps entity."""
if await self._azure_devops_update():
self._available = True
else:
if self._available:
_LOGGER.debug(
"An error occurred while updating Azure DevOps sensor.",
exc_info=True,
)
self._available = False
async def _azure_devops_update(self) -> None:
"""Update Azure DevOps entity."""
raise NotImplementedError()
class AzureDevOpsDeviceEntity(AzureDevOpsEntity):
"""Defines a Azure DevOps device entity."""
@property
def device_info(self) -> Dict[str, Any]:
"""Return device information about this Azure DevOps instance."""
return {
"identifiers": {(DOMAIN, self.organization, self.project,)},
"manufacturer": self.organization,
"name": self.project,
}

View File

@ -0,0 +1,134 @@
"""Config flow to configure the Azure DevOps integration."""
import logging
from aioazuredevops.client import DevOpsClient
import aiohttp
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.azure_devops.const import ( # pylint:disable=unused-import
CONF_ORG,
CONF_PAT,
CONF_PROJECT,
DOMAIN,
)
from homeassistant.config_entries import ConfigFlow
_LOGGER = logging.getLogger(__name__)
class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Azure DevOps config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize config flow."""
self._organization = None
self._project = None
self._pat = None
async def _show_setup_form(self, errors=None):
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_ORG, default=self._organization): str,
vol.Required(CONF_PROJECT, default=self._project): str,
vol.Optional(CONF_PAT): str,
}
),
errors=errors or {},
)
async def _show_reauth_form(self, errors=None):
"""Show the reauth form to the user."""
return self.async_show_form(
step_id="reauth",
description_placeholders={
"project_url": f"{self._organization}/{self._project}"
},
data_schema=vol.Schema({vol.Required(CONF_PAT): str}),
errors=errors or {},
)
async def _check_setup(self):
"""Check the setup of the flow."""
errors = {}
client = DevOpsClient()
try:
if self._pat is not None:
await client.authorize(self._pat, self._organization)
if not client.authorized:
errors["base"] = "authorization_error"
return errors
project_info = await client.get_project(self._organization, self._project)
if project_info is None:
errors["base"] = "project_error"
return errors
except aiohttp.ClientError:
errors["base"] = "connection_error"
return errors
return None
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
if user_input is None:
return await self._show_setup_form(user_input)
self._organization = user_input[CONF_ORG]
self._project = user_input[CONF_PROJECT]
self._pat = user_input.get(CONF_PAT)
await self.async_set_unique_id(f"{self._organization}_{self._project}")
self._abort_if_unique_id_configured()
errors = await self._check_setup()
if errors is not None:
return await self._show_setup_form(errors)
return self._async_create_entry()
async def async_step_reauth(self, user_input):
"""Handle configuration by re-auth."""
if user_input.get(CONF_ORG) and user_input.get(CONF_PROJECT):
self._organization = user_input[CONF_ORG]
self._project = user_input[CONF_PROJECT]
self._pat = user_input[CONF_PAT]
# pylint: disable=no-member
self.context["title_placeholders"] = {
"project_url": f"{self._organization}/{self._project}",
}
await self.async_set_unique_id(f"{self._organization}_{self._project}")
errors = await self._check_setup()
if errors is not None:
return await self._show_reauth_form(errors)
for entry in self._async_current_entries():
if entry.unique_id == self.unique_id:
self.hass.config_entries.async_update_entry(
entry,
data={
CONF_ORG: self._organization,
CONF_PROJECT: self._project,
CONF_PAT: self._pat,
},
)
return self.async_abort(reason="reauth_successful")
def _async_create_entry(self):
"""Handle create entry."""
return self.async_create_entry(
title=f"{self._organization}/{self._project}",
data={
CONF_ORG: self._organization,
CONF_PROJECT: self._project,
CONF_PAT: self._pat,
},
)

View File

@ -0,0 +1,11 @@
"""Constants for the Azure DevOps integration."""
DOMAIN = "azure_devops"
DATA_AZURE_DEVOPS_CLIENT = "azure_devops_client"
DATA_ORG = "organization"
DATA_PROJECT = "project"
DATA_PAT = "personal_access_token"
CONF_ORG = "organization"
CONF_PROJECT = "project"
CONF_PAT = "personal_access_token"

View File

@ -0,0 +1,8 @@
{
"domain": "azure_devops",
"name": "Azure DevOps",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/azure_devops",
"requirements": ["aioazuredevops==1.3.5"],
"codeowners": ["@timmo001"]
}

View File

@ -0,0 +1,148 @@
"""Support for Azure DevOps sensors."""
from datetime import timedelta
import logging
from typing import List
from aioazuredevops.builds import DevOpsBuild
from aioazuredevops.client import DevOpsClient
import aiohttp
from homeassistant.components.azure_devops import AzureDevOpsDeviceEntity
from homeassistant.components.azure_devops.const import (
CONF_ORG,
CONF_PROJECT,
DATA_AZURE_DEVOPS_CLIENT,
DATA_ORG,
DATA_PROJECT,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.typing import HomeAssistantType
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=300)
PARALLEL_UPDATES = 4
BUILDS_QUERY = "?queryOrder=queueTimeDescending&maxBuildsPerDefinition=1"
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
) -> None:
"""Set up Azure DevOps sensor based on a config entry."""
instance_key = f"{DOMAIN}_{entry.data[CONF_ORG]}_{entry.data[CONF_PROJECT]}"
client = hass.data[instance_key][DATA_AZURE_DEVOPS_CLIENT]
organization = entry.data[DATA_ORG]
project = entry.data[DATA_PROJECT]
sensors = []
try:
builds: List[DevOpsBuild] = await client.get_builds(
organization, project, BUILDS_QUERY
)
except aiohttp.ClientError as exception:
_LOGGER.warning(exception)
raise PlatformNotReady from exception
for build in builds:
sensors.append(
AzureDevOpsLatestBuildSensor(client, organization, project, build)
)
async_add_entities(sensors, True)
class AzureDevOpsSensor(AzureDevOpsDeviceEntity):
"""Defines a Azure DevOps sensor."""
def __init__(
self,
client: DevOpsClient,
organization: str,
project: str,
key: str,
name: str,
icon: str,
measurement: str = "",
unit_of_measurement: str = "",
) -> None:
"""Initialize Azure DevOps sensor."""
self._state = None
self._attributes = None
self._available = False
self._unit_of_measurement = unit_of_measurement
self.measurement = measurement
self.client = client
self.organization = organization
self.project = project
self.key = key
super().__init__(organization, project, name, icon)
@property
def unique_id(self) -> str:
"""Return the unique ID for this sensor."""
return "_".join([self.organization, self.key])
@property
def state(self) -> str:
"""Return the state of the sensor."""
return self._state
@property
def device_state_attributes(self) -> object:
"""Return the attributes of the sensor."""
return self._attributes
@property
def unit_of_measurement(self) -> str:
"""Return the unit this state is expressed in."""
return self._unit_of_measurement
class AzureDevOpsLatestBuildSensor(AzureDevOpsSensor):
"""Defines a Azure DevOps card count sensor."""
def __init__(
self, client: DevOpsClient, organization: str, project: str, build: DevOpsBuild
):
"""Initialize Azure DevOps sensor."""
self.build: DevOpsBuild = build
super().__init__(
client,
organization,
project,
f"{build.project.id}_{build.definition.id}_latest_build",
f"{build.project.name} {build.definition.name} Latest Build",
"mdi:pipe",
)
async def _azure_devops_update(self) -> bool:
"""Update Azure DevOps entity."""
try:
build: DevOpsBuild = await self.client.get_build(
self.organization, self.project, self.build.id
)
except aiohttp.ClientError as exception:
_LOGGER.warning(exception)
self._available = False
return False
self._state = build.build_number
self._attributes = {
"definition_id": build.definition.id,
"definition_name": build.definition.name,
"id": build.id,
"reason": build.reason,
"result": build.result,
"source_branch": build.source_branch,
"source_version": build.source_version,
"status": build.status,
"url": build.links.web,
"queue_time": build.queue_time,
"start_time": build.start_time,
"finish_time": build.finish_time,
}
self._available = True
return True

View File

@ -0,0 +1,33 @@
{
"config": {
"flow_title": "Azure DevOps: {project_url}",
"error": {
"authorization_error": "Authorization error. Check you have access to the project and have the correct credentials.",
"connection_error": "Could not connect to Azure DevOps.",
"project_error": "Could not get project info."
},
"step": {
"user": {
"data": {
"organization": "Organization",
"project": "Project",
"personal_access_token": "Personal Access Token (PAT)"
},
"description": "Set up an Azure DevOps instance to access your project. A Personal Access Token is only required for a private project.",
"title": "Add Azure DevOps Project"
},
"reauth": {
"data": {
"personal_access_token": "Personal Access Token (PAT)"
},
"description": "Authentication failed for {project_url}. Please enter your current credentials.",
"title": "Reauthentication"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::data::access_token%] updated successfully"
}
},
"title": "Azure DevOps"
}

View File

@ -0,0 +1,33 @@
{
"config": {
"abort": {
"already_configured": "El compte ja ha estat configurat",
"reauth_successful": "Token d'acc\u00e9s actualitzat correctament"
},
"error": {
"authorization_error": "Error d'autoritzaci\u00f3. Comprova que tens acc\u00e9s al projecte i tens les credencials correctes.",
"connection_error": "No s'ha pogut connectar a Azure DevOps.",
"project_error": "No s'ha pogut obtenir la informaci\u00f3 del projecte."
},
"flow_title": "Azure DevOps: {project_url}",
"step": {
"reauth": {
"data": {
"personal_access_token": "Token d'Acc\u00e9s Personal (PAT)"
},
"description": "L'autenticaci\u00f3 de {project_url} ha fallat. Si us plau, introdueix les teves credencials actuals.",
"title": "Reautenticaci\u00f3"
},
"user": {
"data": {
"organization": "Organitzaci\u00f3",
"personal_access_token": "Token d'Acc\u00e9s Personal (PAT)",
"project": "Projecte"
},
"description": "Configura una inst\u00e0ncia d'Azure DevOps per accedir al teu projecte. El token d'acc\u00e9s personal nom\u00e9s \u00e9s necessari per a projectes privats.",
"title": "Afegeix un projecte Azure DevOps"
}
}
},
"title": "Azure DevOps"
}

View File

@ -0,0 +1,33 @@
{
"config": {
"abort": {
"already_configured": "Account is already configured",
"reauth_successful": "Access Token updated successfully"
},
"error": {
"authorization_error": "Authorization error. Check you have access to the project and have the correct credentials.",
"connection_error": "Could not connect to Azure DevOps.",
"project_error": "Could not get project info."
},
"flow_title": "Azure DevOps: {project_url}",
"step": {
"reauth": {
"data": {
"personal_access_token": "Personal Access Token (PAT)"
},
"description": "Authentication failed for {project_url}. Please enter your current credentials.",
"title": "Reauthentication"
},
"user": {
"data": {
"organization": "Organization",
"personal_access_token": "Personal Access Token (PAT)",
"project": "Project"
},
"description": "Set up an Azure DevOps instance to access your project. A Personal Access Token is only required for a private project.",
"title": "Add Azure DevOps Project"
}
}
},
"title": "Azure DevOps"
}

View File

@ -0,0 +1,33 @@
{
"config": {
"abort": {
"already_configured": "La cuenta ya ha sido configurada",
"reauth_successful": "Token de acceso actualizado correctamente "
},
"error": {
"authorization_error": "Error de autorizaci\u00f3n. Comprueba que tienes acceso al proyecto y las credenciales son correctas.",
"connection_error": "No se pudo conectar con Azure DevOps",
"project_error": "No se pudo obtener informaci\u00f3n del proyecto."
},
"flow_title": "Azure DevOps: {project_url}",
"step": {
"reauth": {
"data": {
"personal_access_token": "Token Personal de Acceso (PAT)"
},
"description": "Error de autenticaci\u00f3n para {project_url}. Por favor, introduce tus credenciales actuales.",
"title": "Reautenticaci\u00f3n"
},
"user": {
"data": {
"organization": "Organizaci\u00f3n",
"personal_access_token": "Token Personal de Acceso (PAT)",
"project": "Proyecto"
},
"description": "Configura una instancia de Azure DevOps para acceder a tu proyecto. Un Token Personal de Acceso s\u00f3lo es necesario para un proyecto privado.",
"title": "A\u00f1adir Proyecto Azure DevOps"
}
}
},
"title": "Azure DevOps"
}

View File

@ -0,0 +1,33 @@
{
"config": {
"abort": {
"already_configured": "L'account \u00e8 gi\u00e0 configurato",
"reauth_successful": "Token di accesso aggiornato correttamente"
},
"error": {
"authorization_error": "Errore di autorizzazione. Verificare di avere accesso al progetto e disporre delle credenziali corrette.",
"connection_error": "Impossibile connettersi ad Azure DevOps.",
"project_error": "Non \u00e8 stato possibile ottenere informazioni sul progetto."
},
"flow_title": "Azure DevOps: {project_url}",
"step": {
"reauth": {
"data": {
"personal_access_token": "Token di Accesso Personale (PAT)"
},
"description": "Autenticazione non riuscita per {project_url}. Si prega di inserire le proprie credenziali attuali.",
"title": "Riautenticazione"
},
"user": {
"data": {
"organization": "Organizzazione",
"personal_access_token": "Token di Accesso Personale (PAT)",
"project": "Progetto"
},
"description": "Configurare un'istanza di DevOps di Azure per accedere al progetto. Un Token di Accesso Personale (PAT) \u00e8 richiesto solo per un progetto privato.",
"title": "Aggiungere un progetto Azure DevOps"
}
}
},
"title": "Azure DevOps"
}

View File

@ -0,0 +1,33 @@
{
"config": {
"abort": {
"already_configured": "Kont ass scho konfigur\u00e9iert",
"reauth_successful": "Acc\u00e8s Jeton erfollegr\u00e4ich aktualis\u00e9iert"
},
"error": {
"authorization_error": "Feeler bei der Authorisatioun. Iwwerpr\u00e9if ob d\u00e4in Kont den acc\u00e8s zum Projet souw\u00e9i d\u00e9i richteg Umeldungsinformatioune huet",
"connection_error": "Konnt sech net mat Azure DevOps verbannen",
"project_error": "Konnt keng Projet Informatiounen ausliesen."
},
"flow_title": "Azure DevOps: {project_url}",
"step": {
"reauth": {
"data": {
"personal_access_token": "Pers\u00e9inlechen Acc\u00e8s Jeton (PAT)"
},
"description": "Feeler bei der Authentifikatioun fir {project_url}. G\u00ebff deng aktuell Umeldungsinformatiounen an.",
"title": "Reauthentifikatioun"
},
"user": {
"data": {
"organization": "Organisatioun",
"personal_access_token": "Pers\u00e9inlechen Acc\u00e8s Jeton (PAT)",
"project": "Projet"
},
"description": "Riicht eng Azure DevOps Instanz an fir d\u00e4in Projet z'acc\u00e9d\u00e9ieren. E Pers\u00e9inlechen Acc\u00e8s Jetons ass n\u00ebmme fir ee Private Projet n\u00e9ideg.",
"title": "Azure DevOps Project dob\u00e4isetzen"
}
}
},
"title": "Azure DevOps"
}

View File

@ -0,0 +1,33 @@
{
"config": {
"abort": {
"already_configured": "Kontoen er allerede konfigurert",
"reauth_successful": "Tilgangstoken oppdatert"
},
"error": {
"authorization_error": "Autoriseringsfeil. Sjekk at du har tilgang til prosjektet og har riktig legitimasjon.",
"connection_error": "Kunne ikke koble til Azure DevOps.",
"project_error": "Kunne ikke f\u00e5 prosjektinformasjon."
},
"flow_title": "Azure DevOps: {project_url}",
"step": {
"reauth": {
"data": {
"personal_access_token": "Token for personlig tilgang (PAT)"
},
"description": "Autentiseringen mislyktes for {project_url} . Vennligst skriv inn gjeldende legitimasjon.",
"title": "reautentisering"
},
"user": {
"data": {
"organization": "Organisasjon",
"personal_access_token": "Token for personlig tilgang (PAT)",
"project": "Prosjekt"
},
"description": "Sett opp en Azure DevOps-forekomst for \u00e5 f\u00e5 tilgang til prosjektet ditt. En personlig tilgangstoken er bare n\u00f8dvendig for et privat prosjekt.",
"title": "Legg til Azure DevOps Project"
}
}
},
"title": "Azure DevOps"
}

View File

@ -0,0 +1,33 @@
{
"config": {
"abort": {
"already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.",
"reauth_successful": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d."
},
"error": {
"authorization_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443 \u0412\u0430\u0441 \u0435\u0441\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043f\u0440\u043e\u0435\u043a\u0442\u0443, \u0430 \u0442\u0430\u043a \u0436\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
"connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a Azure DevOps.",
"project_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0435."
},
"flow_title": "Azure DevOps: {project_url}",
"step": {
"reauth": {
"data": {
"personal_access_token": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 (PAT)"
},
"description": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 {project_url}. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
"title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
},
"user": {
"data": {
"organization": "\u041e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f",
"personal_access_token": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 (PAT)",
"project": "\u041f\u0440\u043e\u0435\u043a\u0442"
},
"description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c Azure DevOps. \u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0430\u0441\u0442\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432.",
"title": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0435\u043a\u0442 Azure DevOps"
}
}
},
"title": "Azure DevOps"
}

View File

@ -0,0 +1,19 @@
{
"config": {
"flow_title": "Azure DevOps: {project_url}",
"step": {
"reauth": {
"title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f"
},
"user": {
"data": {
"organization": "\u041e\u0440\u0433\u0430\u043d\u0456\u0437\u0430\u0446\u0456\u044f",
"personal_access_token": "\u0422\u043e\u043a\u0435\u043d \u043e\u0441\u043e\u0431\u0438\u0441\u0442\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (PAT)",
"project": "\u041f\u0440\u043e\u0454\u043a\u0442"
},
"title": "\u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u043e\u0435\u043a\u0442 Azure DevOps"
}
}
},
"title": "Azure DevOps"
}

View File

@ -0,0 +1,33 @@
{
"config": {
"abort": {
"already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"reauth_successful": "\u5b58\u53d6\u5bc6\u9470\u5df2\u6210\u529f\u66f4\u65b0"
},
"error": {
"authorization_error": "\u8a8d\u8b49\u932f\u8aa4\u3002\u8acb\u78ba\u8a8d\u64c1\u6709\u5c08\u6848\u5b58\u53d6\u6b0a\u8207\u6b63\u78ba\u7684\u8b49\u66f8\u3002",
"connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 Azure DevOps\u3002",
"project_error": "\u7121\u6cd5\u53d6\u5f97\u5c08\u6848\u8cc7\u8a0a\u3002"
},
"flow_title": "Azure DevOps\uff1a{project_url}",
"step": {
"reauth": {
"data": {
"personal_access_token": "\u500b\u4eba\u5b58\u53d6\u5bc6\u9470\uff08PAT\uff09"
},
"description": "{project_url}\u8a8d\u8b49\u5931\u6557\u3002\u8acb\u8f38\u5165\u76ee\u524d\u8b49\u66f8\u3002",
"title": "\u91cd\u65b0\u8a8d\u8b49"
},
"user": {
"data": {
"organization": "\u7d44\u7e54",
"personal_access_token": "\u500b\u4eba\u5b58\u53d6\u5bc6\u9470\uff08PAT\uff09",
"project": "\u5c08\u6848"
},
"description": "\u8a2d\u5b9a Azure DevOps \u4ee5\u5b58\u53d6\u5c08\u6848\u3002\u79c1\u4eba\u5c08\u6848\u5247\u9700\u8981\u8f38\u5165\u300c\u500b\u4eba\u5b58\u53d6\u5bc6\u9470\uff09\u3002",
"title": "\u65b0\u589e Azure DevOps \u5c08\u6848"
}
}
},
"title": "Azure DevOps"
}

View File

@ -131,7 +131,7 @@
"on": "H\u00famedo" "on": "H\u00famedo"
}, },
"motion": { "motion": {
"off": "Sin movimiento", "off": "No detectado",
"on": "Detectado" "on": "Detectado"
}, },
"occupancy": { "occupancy": {

View File

@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "IP-adres",
"port": "Poort"
}
}
}
}
}

View File

@ -14,7 +14,7 @@
"user": { "user": {
"data": { "data": {
"host": "IP adresse", "host": "IP adresse",
"port": "Port" "port": ""
}, },
"description": "Konfigurer BleBox-en til \u00e5 integreres med Home Assistant.", "description": "Konfigurer BleBox-en til \u00e5 integreres med Home Assistant.",
"title": "Konfigurere BleBox-enheten" "title": "Konfigurere BleBox-enheten"

View File

@ -1,32 +1,25 @@
"""Support for Blink Home Camera System.""" """Support for Blink Home Camera System."""
import asyncio import asyncio
from copy import deepcopy
import logging import logging
from blinkpy.auth import Auth
from blinkpy.blinkpy import Blink from blinkpy.blinkpy import Blink
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components import persistent_notification
from homeassistant.const import ( from homeassistant.components.blink.const import (
CONF_FILENAME,
CONF_NAME,
CONF_PASSWORD,
CONF_PIN,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from .const import (
DEFAULT_OFFSET,
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
DEVICE_ID,
DOMAIN, DOMAIN,
PLATFORMS, PLATFORMS,
SERVICE_REFRESH, SERVICE_REFRESH,
SERVICE_SAVE_VIDEO, SERVICE_SAVE_VIDEO,
SERVICE_SEND_PIN, SERVICE_SEND_PIN,
) )
from homeassistant.const import CONF_FILENAME, CONF_NAME, CONF_PIN, CONF_SCAN_INTERVAL
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -35,58 +28,50 @@ SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
) )
SERVICE_SEND_PIN_SCHEMA = vol.Schema({vol.Optional(CONF_PIN): cv.string}) SERVICE_SEND_PIN_SCHEMA = vol.Schema({vol.Optional(CONF_PIN): cv.string})
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int,
}
)
},
extra=vol.ALLOW_EXTRA,
)
def _blink_startup_wrapper(hass, entry):
def _blink_startup_wrapper(entry):
"""Startup wrapper for blink.""" """Startup wrapper for blink."""
blink = Blink( blink = Blink()
username=entry.data[CONF_USERNAME], auth_data = deepcopy(dict(entry.data))
password=entry.data[CONF_PASSWORD], blink.auth = Auth(auth_data, no_prompt=True)
motion_interval=DEFAULT_OFFSET,
legacy_subdomain=False,
no_prompt=True,
device_id=DEVICE_ID,
)
blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
try: if blink.start():
blink.login_response = entry.data["login_response"]
blink.setup_params(entry.data["login_response"])
except KeyError:
blink.get_auth_token()
blink.setup_params(entry.data["login_response"])
blink.setup_post_verify() blink.setup_post_verify()
elif blink.auth.check_key_required():
_LOGGER.debug("Attempting a reauth flow")
_reauth_flow_wrapper(hass, auth_data)
return blink return blink
def _reauth_flow_wrapper(hass, data):
"""Reauth flow wrapper."""
hass.add_job(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": "reauth"}, data=data
)
)
persistent_notification.async_create(
hass,
"Blink configuration migrated to a new version. Please go to the integrations page to re-configure (such as sending a new 2FA key).",
"Blink Migration",
)
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up a config entry.""" """Set up a Blink component."""
hass.data[DOMAIN] = {} hass.data[DOMAIN] = {}
if DOMAIN not in config:
return True return True
conf = config.get(DOMAIN, {})
if not hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
)
async def async_migrate_entry(hass, entry):
"""Handle migration of a previous version config entry."""
data = {**entry.data}
if entry.version == 1:
data.pop("login_response", None)
await hass.async_add_executor_job(_reauth_flow_wrapper, hass, data)
return False
return True return True
@ -95,12 +80,11 @@ async def async_setup_entry(hass, entry):
_async_import_options_from_data_if_missing(hass, entry) _async_import_options_from_data_if_missing(hass, entry)
hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job( hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job(
_blink_startup_wrapper, entry _blink_startup_wrapper, hass, entry
) )
if not hass.data[DOMAIN][entry.entry_id].available: if not hass.data[DOMAIN][entry.entry_id].available:
_LOGGER.error("Blink unavailable for setup") raise ConfigEntryNotReady
return False
for component in PLATFORMS: for component in PLATFORMS:
hass.async_create_task( hass.async_create_task(
@ -118,7 +102,7 @@ async def async_setup_entry(hass, entry):
def send_pin(call): def send_pin(call):
"""Call blink to send new pin.""" """Call blink to send new pin."""
pin = call.data[CONF_PIN] pin = call.data[CONF_PIN]
hass.data[DOMAIN][entry.entry_id].login_handler.send_auth_key( hass.data[DOMAIN][entry.entry_id].auth.send_auth_key(
hass.data[DOMAIN][entry.entry_id], pin, hass.data[DOMAIN][entry.entry_id], pin,
) )

View File

@ -1,10 +1,16 @@
"""Config flow to configure Blink.""" """Config flow to configure Blink."""
import logging import logging
from blinkpy.blinkpy import Blink from blinkpy.auth import Auth, LoginError, TokenRefreshFailed
from blinkpy.blinkpy import Blink, BlinkSetupError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, core, exceptions from homeassistant import config_entries, core, exceptions
from homeassistant.components.blink.const import (
DEFAULT_SCAN_INTERVAL,
DEVICE_ID,
DOMAIN,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_PASSWORD,
CONF_PIN, CONF_PIN,
@ -13,36 +19,36 @@ from homeassistant.const import (
) )
from homeassistant.core import callback from homeassistant.core import callback
from .const import DEFAULT_OFFSET, DEFAULT_SCAN_INTERVAL, DEVICE_ID, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def validate_input(hass: core.HomeAssistant, blink): def validate_input(hass: core.HomeAssistant, auth):
"""Validate the user input allows us to connect.""" """Validate the user input allows us to connect."""
response = await hass.async_add_executor_job(blink.get_auth_token) try:
if not response: auth.startup()
except (LoginError, TokenRefreshFailed):
raise InvalidAuth raise InvalidAuth
if blink.key_required: if auth.check_key_required():
raise Require2FA raise Require2FA
return blink.login_response
def _send_blink_2fa_pin(auth, pin):
"""Send 2FA pin to blink servers."""
blink = Blink()
blink.auth = auth
blink.setup_urls()
return auth.send_auth_key(blink, pin)
class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Blink config flow.""" """Handle a Blink config flow."""
VERSION = 1 VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self): def __init__(self):
"""Initialize the blink flow.""" """Initialize the blink flow."""
self.blink = None self.auth = None
self.data = {
CONF_USERNAME: "",
CONF_PASSWORD: "",
"login_response": None,
}
@staticmethod @staticmethod
@callback @callback
@ -53,28 +59,19 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user.""" """Handle a flow initiated by the user."""
errors = {} errors = {}
data = {CONF_USERNAME: "", CONF_PASSWORD: "", "device_id": DEVICE_ID}
if user_input is not None: if user_input is not None:
self.data[CONF_USERNAME] = user_input["username"] data[CONF_USERNAME] = user_input["username"]
self.data[CONF_PASSWORD] = user_input["password"] data[CONF_PASSWORD] = user_input["password"]
await self.async_set_unique_id(self.data[CONF_USERNAME]) self.auth = Auth(data, no_prompt=True)
await self.async_set_unique_id(data[CONF_USERNAME])
if CONF_SCAN_INTERVAL in user_input:
self.data[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL]
self.blink = Blink(
username=self.data[CONF_USERNAME],
password=self.data[CONF_PASSWORD],
motion_interval=DEFAULT_OFFSET,
legacy_subdomain=False,
no_prompt=True,
device_id=DEVICE_ID,
)
try: try:
response = await validate_input(self.hass, self.blink) await self.hass.async_add_executor_job(
self.data["login_response"] = response validate_input, self.hass, self.auth
return self.async_create_entry(title=DOMAIN, data=self.data,) )
return self._async_finish_flow()
except Require2FA: except Require2FA:
return await self.async_step_2fa() return await self.async_step_2fa()
except InvalidAuth: except InvalidAuth:
@ -94,23 +91,40 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_2fa(self, user_input=None): async def async_step_2fa(self, user_input=None):
"""Handle 2FA step.""" """Handle 2FA step."""
errors = {}
if user_input is not None: if user_input is not None:
pin = user_input.get(CONF_PIN) pin = user_input.get(CONF_PIN)
if await self.hass.async_add_executor_job( try:
self.blink.login_handler.send_auth_key, self.blink, pin valid_token = await self.hass.async_add_executor_job(
): _send_blink_2fa_pin, self.auth, pin
return await self.async_step_user(user_input=self.data) )
except BlinkSetupError:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
if valid_token:
return self._async_finish_flow()
errors["base"] = "invalid_access_token"
return self.async_show_form( return self.async_show_form(
step_id="2fa", step_id="2fa",
data_schema=vol.Schema( data_schema=vol.Schema(
{vol.Optional("pin"): vol.All(str, vol.Length(min=1))} {vol.Optional("pin"): vol.All(str, vol.Length(min=1))}
), ),
errors=errors,
) )
async def async_step_import(self, import_data): async def async_step_reauth(self, entry_data):
"""Import blink config from configuration.yaml.""" """Perform reauth upon migration of old entries."""
return await self.async_step_user(import_data) return await self.async_step_user(entry_data)
@callback
def _async_finish_flow(self):
"""Finish with setup."""
return self.async_create_entry(title=DOMAIN, data=self.auth.login_attributes)
class BlinkOptionsFlowHandler(config_entries.OptionsFlow): class BlinkOptionsFlowHandler(config_entries.OptionsFlow):

View File

@ -2,6 +2,7 @@
DOMAIN = "blink" DOMAIN = "blink"
DEVICE_ID = "Home Assistant" DEVICE_ID = "Home Assistant"
CONF_MIGRATE = "migrate"
CONF_CAMERA = "camera" CONF_CAMERA = "camera"
CONF_ALARM_CONTROL_PANEL = "alarm_control_panel" CONF_ALARM_CONTROL_PANEL = "alarm_control_panel"

View File

@ -2,7 +2,7 @@
"domain": "blink", "domain": "blink",
"name": "Blink", "name": "Blink",
"documentation": "https://www.home-assistant.io/integrations/blink", "documentation": "https://www.home-assistant.io/integrations/blink",
"requirements": ["blinkpy==0.15.1"], "requirements": ["blinkpy==0.16.3"],
"codeowners": ["@fronzbot"], "codeowners": ["@fronzbot"],
"config_flow": true "config_flow": true
} }

View File

@ -11,11 +11,13 @@
"2fa": { "2fa": {
"title": "Two-factor authentication", "title": "Two-factor authentication",
"data": { "2fa": "Two-factor code" }, "data": { "2fa": "Two-factor code" },
"description": "Enter the pin sent to your email. If the email does not contain a pin, leave blank" "description": "Enter the pin sent to your email"
} }
}, },
"error": { "error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"abort": { "abort": {

View File

@ -1,17 +1,24 @@
"""The Bond integration.""" """The Bond integration."""
import asyncio import asyncio
from asyncio import TimeoutError as AsyncIOTimeoutError
import logging
from bond import Bond from aiohttp import ClientError, ClientTimeout
from bond_api import Bond
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import SLOW_UPDATE_WARNING
from .const import DOMAIN from .const import DOMAIN
from .utils import BondHub from .utils import BondHub
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["cover", "fan", "light", "switch"] PLATFORMS = ["cover", "fan", "light", "switch"]
_API_TIMEOUT = SLOW_UPDATE_WARNING - 1
async def async_setup(hass: HomeAssistant, config: dict): async def async_setup(hass: HomeAssistant, config: dict):
@ -25,11 +32,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
host = entry.data[CONF_HOST] host = entry.data[CONF_HOST]
token = entry.data[CONF_ACCESS_TOKEN] token = entry.data[CONF_ACCESS_TOKEN]
bond = Bond(bondIp=host, bondToken=token) bond = Bond(host=host, token=token, timeout=ClientTimeout(total=_API_TIMEOUT))
hub = BondHub(bond) hub = BondHub(bond)
await hass.async_add_executor_job(hub.setup) try:
await hub.setup()
except (ClientError, AsyncIOTimeoutError, OSError) as error:
raise ConfigEntryNotReady from error
hass.data[DOMAIN][entry.entry_id] = hub hass.data[DOMAIN][entry.entry_id] = hub
if not entry.unique_id:
hass.config_entries.async_update_entry(entry, unique_id=hub.bond_id)
device_registry = await dr.async_get_registry(hass) device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create( device_registry.async_get_or_create(
config_entry_id=entry.entry_id, config_entry_id=entry.entry_id,

View File

@ -1,42 +1,45 @@
"""Config flow for Bond integration.""" """Config flow for Bond integration."""
from json import JSONDecodeError
import logging import logging
from typing import Any, Dict, Optional
from bond import Bond from aiohttp import ClientConnectionError, ClientResponseError
from requests.exceptions import ConnectionError as RequestConnectionError from bond_api import Bond
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, core, exceptions from homeassistant import config_entries, exceptions
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME
from .const import CONF_BOND_ID
from .const import DOMAIN # pylint:disable=unused-import from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema( DATA_SCHEMA_USER = vol.Schema(
{vol.Required(CONF_HOST): str, vol.Required(CONF_ACCESS_TOKEN): str} {vol.Required(CONF_HOST): str, vol.Required(CONF_ACCESS_TOKEN): str}
) )
DATA_SCHEMA_DISCOVERY = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
async def validate_input(hass: core.HomeAssistant, data): async def _validate_input(data: Dict[str, Any]) -> str:
"""Validate the user input allows us to connect.""" """Validate the user input allows us to connect."""
def authenticate(bond_hub: Bond) -> bool:
try: try:
bond_hub.getDeviceIds()
return True
except RequestConnectionError:
raise CannotConnect
except JSONDecodeError:
return False
bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN]) bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN])
version = await bond.version()
# call to non-version API is needed to validate authentication
await bond.devices()
except ClientConnectionError:
raise InputValidationError("cannot_connect")
except ClientResponseError as error:
if error.status == 401:
raise InputValidationError("invalid_auth")
raise InputValidationError("unknown")
except Exception:
_LOGGER.exception("Unexpected exception")
raise InputValidationError("unknown")
if not await hass.async_add_executor_job(authenticate, bond): # Return unique ID from the hub to be stored in the config entry.
raise InvalidAuth return version["bondid"]
# Return info that you want to store in the config entry.
return {"title": data[CONF_HOST]}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@ -45,30 +48,73 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
async def async_step_user(self, user_input=None): _discovered: dict = None
"""Handle the initial step."""
async def async_step_zeroconf(
self, discovery_info: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Handle a flow initialized by zeroconf discovery."""
name: str = discovery_info[CONF_NAME]
host: str = discovery_info[CONF_HOST]
bond_id = name.partition(".")[0]
await self.async_set_unique_id(bond_id)
self._abort_if_unique_id_configured({CONF_HOST: host})
self._discovered = {
CONF_HOST: host,
CONF_BOND_ID: bond_id,
}
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": self._discovered})
return await self.async_step_confirm()
async def async_step_confirm(
self, user_input: Dict[str, Any] = None
) -> Dict[str, Any]:
"""Handle confirmation flow for discovered bond hub."""
errors = {}
if user_input is not None:
data = user_input.copy()
data[CONF_HOST] = self._discovered[CONF_HOST]
try:
return await self._try_create_entry(data)
except InputValidationError as error:
errors["base"] = error.base
return self.async_show_form(
step_id="confirm",
data_schema=DATA_SCHEMA_DISCOVERY,
errors=errors,
description_placeholders=self._discovered,
)
async def async_step_user(
self, user_input: Dict[str, Any] = None
) -> Dict[str, Any]:
"""Handle a flow initialized by the user."""
errors = {} errors = {}
if user_input is not None: if user_input is not None:
try: try:
info = await validate_input(self.hass, user_input) return await self._try_create_entry(user_input)
except CannotConnect: except InputValidationError as error:
errors["base"] = "cannot_connect" errors["base"] = error.base
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors
) )
async def _try_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]:
class CannotConnect(exceptions.HomeAssistantError): bond_id = await _validate_input(data)
"""Error to indicate we cannot connect.""" await self.async_set_unique_id(bond_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=bond_id, data=data)
class InvalidAuth(exceptions.HomeAssistantError): class InputValidationError(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth.""" """Error to indicate we cannot proceed due to invalid input."""
def __init__(self, base: str):
"""Initialize with error base."""
super().__init__()
self.base = base

View File

@ -1,3 +1,5 @@
"""Constants for the Bond integration.""" """Constants for the Bond integration."""
DOMAIN = "bond" DOMAIN = "bond"
CONF_BOND_ID: str = "bond_id"

View File

@ -1,7 +1,7 @@
"""Support for Bond covers.""" """Support for Bond covers."""
from typing import Any, Callable, List, Optional from typing import Any, Callable, List, Optional
from bond import DeviceTypes from bond_api import Action, DeviceType
from homeassistant.components.cover import DEVICE_CLASS_SHADE, CoverEntity from homeassistant.components.cover import DEVICE_CLASS_SHADE, CoverEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -21,12 +21,10 @@ async def async_setup_entry(
"""Set up Bond cover devices.""" """Set up Bond cover devices."""
hub: BondHub = hass.data[DOMAIN][entry.entry_id] hub: BondHub = hass.data[DOMAIN][entry.entry_id]
devices = await hass.async_add_executor_job(hub.get_bond_devices)
covers = [ covers = [
BondCover(hub, device) BondCover(hub, device)
for device in devices for device in hub.devices
if device.type == DeviceTypes.MOTORIZED_SHADES if device.type == DeviceType.MOTORIZED_SHADES
] ]
async_add_entities(covers, True) async_add_entities(covers, True)
@ -41,30 +39,28 @@ class BondCover(BondEntity, CoverEntity):
self._closed: Optional[bool] = None self._closed: Optional[bool] = None
def _apply_state(self, state: dict):
cover_open = state.get("open")
self._closed = True if cover_open == 0 else False if cover_open == 1 else None
@property @property
def device_class(self) -> Optional[str]: def device_class(self) -> Optional[str]:
"""Get device class.""" """Get device class."""
return DEVICE_CLASS_SHADE return DEVICE_CLASS_SHADE
def update(self):
"""Fetch assumed state of the cover from the hub using API."""
state: dict = self._hub.bond.getDeviceState(self._device.device_id)
cover_open = state.get("open")
self._closed = True if cover_open == 0 else False if cover_open == 1 else None
@property @property
def is_closed(self): def is_closed(self):
"""Return if the cover is closed or not.""" """Return if the cover is closed or not."""
return self._closed return self._closed
def open_cover(self, **kwargs: Any) -> None: async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover.""" """Open the cover."""
self._hub.bond.open(self._device.device_id) await self._hub.bond.action(self._device.device_id, Action.open())
def close_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover.""" """Close cover."""
self._hub.bond.close(self._device.device_id) await self._hub.bond.action(self._device.device_id, Action.close())
def stop_cover(self, **kwargs): async def async_stop_cover(self, **kwargs):
"""Hold cover.""" """Hold cover."""
self._hub.bond.hold(self._device.device_id) await self._hub.bond.action(self._device.device_id, Action.hold())

View File

@ -1,24 +1,35 @@
"""An abstract class common to all Bond entities.""" """An abstract class common to all Bond entities."""
from abc import abstractmethod
from asyncio import TimeoutError as AsyncIOTimeoutError
import logging
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from aiohttp import ClientError
from homeassistant.const import ATTR_NAME from homeassistant.const import ATTR_NAME
from homeassistant.helpers.entity import Entity
from .const import DOMAIN from .const import DOMAIN
from .utils import BondDevice, BondHub from .utils import BondDevice, BondHub
_LOGGER = logging.getLogger(__name__)
class BondEntity:
class BondEntity(Entity):
"""Generic Bond entity encapsulating common features of any Bond controlled device.""" """Generic Bond entity encapsulating common features of any Bond controlled device."""
def __init__(self, hub: BondHub, device: BondDevice): def __init__(self, hub: BondHub, device: BondDevice):
"""Initialize entity with API and device info.""" """Initialize entity with API and device info."""
self._hub = hub self._hub = hub
self._device = device self._device = device
self._available = True
@property @property
def unique_id(self) -> Optional[str]: def unique_id(self) -> Optional[str]:
"""Get unique ID for the entity.""" """Get unique ID for the entity."""
return self._device.device_id hub_id = self._hub.bond_id
device_id = self._device.device_id
return f"{hub_id}_{device_id}"
@property @property
def name(self) -> Optional[str]: def name(self) -> Optional[str]:
@ -37,4 +48,30 @@ class BondEntity:
@property @property
def assumed_state(self) -> bool: def assumed_state(self) -> bool:
"""Let HA know this entity relies on an assumed state tracked by Bond.""" """Let HA know this entity relies on an assumed state tracked by Bond."""
return True return self._hub.is_bridge and not self._device.trust_state
@property
def available(self) -> bool:
"""Report availability of this entity based on last API call results."""
return self._available
async def async_update(self):
"""Fetch assumed state of the cover from the hub using API."""
try:
state: dict = await self._hub.bond.device_state(self._device.device_id)
except (ClientError, AsyncIOTimeoutError, OSError) as error:
if self._available:
_LOGGER.warning(
"Entity %s has become unavailable", self.entity_id, exc_info=error
)
self._available = False
else:
_LOGGER.debug("Device state for %s is:\n%s", self.entity_id, state)
if not self._available:
_LOGGER.info("Entity %s has come back", self.entity_id)
self._available = True
self._apply_state(state)
@abstractmethod
def _apply_state(self, state: dict):
raise NotImplementedError

View File

@ -2,7 +2,7 @@
import math import math
from typing import Any, Callable, List, Optional from typing import Any, Callable, List, Optional
from bond import DeviceTypes, Directions from bond_api import Action, DeviceType, Direction
from homeassistant.components.fan import ( from homeassistant.components.fan import (
DIRECTION_FORWARD, DIRECTION_FORWARD,
@ -32,12 +32,8 @@ async def async_setup_entry(
"""Set up Bond fan devices.""" """Set up Bond fan devices."""
hub: BondHub = hass.data[DOMAIN][entry.entry_id] hub: BondHub = hass.data[DOMAIN][entry.entry_id]
devices = await hass.async_add_executor_job(hub.get_bond_devices)
fans = [ fans = [
BondFan(hub, device) BondFan(hub, device) for device in hub.devices if DeviceType.is_fan(device.type)
for device in devices
if device.type == DeviceTypes.CEILING_FAN
] ]
async_add_entities(fans, True) async_add_entities(fans, True)
@ -54,6 +50,11 @@ class BondFan(BondEntity, FanEntity):
self._speed: Optional[int] = None self._speed: Optional[int] = None
self._direction: Optional[int] = None self._direction: Optional[int] = None
def _apply_state(self, state: dict):
self._power = state.get("power")
self._speed = state.get("speed")
self._direction = state.get("direction")
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
@ -74,7 +75,7 @@ class BondFan(BondEntity, FanEntity):
return None return None
# map 1..max_speed Bond speed to 1..3 HA speed # map 1..max_speed Bond speed to 1..3 HA speed
max_speed = self._device.props.get("max_speed", 3) max_speed = max(self._device.props.get("max_speed", 3), self._speed)
ha_speed = math.ceil(self._speed * (len(self.speed_list) - 1) / max_speed) ha_speed = math.ceil(self._speed * (len(self.speed_list) - 1) / max_speed)
return self.speed_list[ha_speed] return self.speed_list[ha_speed]
@ -87,21 +88,14 @@ class BondFan(BondEntity, FanEntity):
def current_direction(self) -> Optional[str]: def current_direction(self) -> Optional[str]:
"""Return fan rotation direction.""" """Return fan rotation direction."""
direction = None direction = None
if self._direction == Directions.FORWARD: if self._direction == Direction.FORWARD:
direction = DIRECTION_FORWARD direction = DIRECTION_FORWARD
elif self._direction == Directions.REVERSE: elif self._direction == Direction.REVERSE:
direction = DIRECTION_REVERSE direction = DIRECTION_REVERSE
return direction return direction
def update(self): async def async_set_speed(self, speed: str) -> None:
"""Fetch assumed state of the fan from the hub using API."""
state: dict = self._hub.bond.getDeviceState(self._device.device_id)
self._power = state.get("power")
self._speed = state.get("speed")
self._direction = state.get("direction")
def set_speed(self, speed: str) -> None:
"""Set the desired speed for the fan.""" """Set the desired speed for the fan."""
max_speed = self._device.props.get("max_speed", 3) max_speed = self._device.props.get("max_speed", 3)
if speed == SPEED_LOW: if speed == SPEED_LOW:
@ -110,21 +104,27 @@ class BondFan(BondEntity, FanEntity):
bond_speed = max_speed bond_speed = max_speed
else: else:
bond_speed = math.ceil(max_speed / 2) bond_speed = math.ceil(max_speed / 2)
self._hub.bond.setSpeed(self._device.device_id, speed=bond_speed)
def turn_on(self, speed: Optional[str] = None, **kwargs) -> None: await self._hub.bond.action(
self._device.device_id, Action.set_speed(bond_speed)
)
async def async_turn_on(self, speed: Optional[str] = None, **kwargs) -> None:
"""Turn on the fan.""" """Turn on the fan."""
if speed is not None: if speed is not None:
self.set_speed(speed) await self.async_set_speed(speed)
self._hub.bond.turnOn(self._device.device_id) else:
await self._hub.bond.action(self._device.device_id, Action.turn_on())
def turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fan off.""" """Turn the fan off."""
self._hub.bond.turnOff(self._device.device_id) await self._hub.bond.action(self._device.device_id, Action.turn_off())
def set_direction(self, direction: str) -> None: async def async_set_direction(self, direction: str):
"""Set fan rotation direction.""" """Set fan rotation direction."""
bond_direction = ( bond_direction = (
Directions.REVERSE if direction == DIRECTION_REVERSE else Directions.FORWARD Direction.REVERSE if direction == DIRECTION_REVERSE else Direction.FORWARD
)
await self._hub.bond.action(
self._device.device_id, Action.set_direction(bond_direction)
) )
self._hub.bond.setDirection(self._device.device_id, bond_direction)

View File

@ -1,7 +1,7 @@
"""Support for Bond lights.""" """Support for Bond lights."""
from typing import Any, Callable, List, Optional from typing import Any, Callable, List, Optional
from bond import DeviceTypes from bond_api import Action, DeviceType
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
@ -26,21 +26,19 @@ async def async_setup_entry(
"""Set up Bond light devices.""" """Set up Bond light devices."""
hub: BondHub = hass.data[DOMAIN][entry.entry_id] hub: BondHub = hass.data[DOMAIN][entry.entry_id]
devices = await hass.async_add_executor_job(hub.get_bond_devices) lights: List[Entity] = [
lights = [
BondLight(hub, device) BondLight(hub, device)
for device in devices for device in hub.devices
if device.type == DeviceTypes.CEILING_FAN and device.supports_light() if DeviceType.is_fan(device.type) and device.supports_light()
] ]
async_add_entities(lights, True)
fireplaces = [ fireplaces: List[Entity] = [
BondFireplace(hub, device) BondFireplace(hub, device)
for device in devices for device in hub.devices
if device.type == DeviceTypes.FIREPLACE if DeviceType.is_fireplace(device.type)
] ]
async_add_entities(fireplaces, True)
async_add_entities(lights + fireplaces, True)
class BondLight(BondEntity, LightEntity): class BondLight(BondEntity, LightEntity):
@ -49,26 +47,49 @@ class BondLight(BondEntity, LightEntity):
def __init__(self, hub: BondHub, device: BondDevice): def __init__(self, hub: BondHub, device: BondDevice):
"""Create HA entity representing Bond fan.""" """Create HA entity representing Bond fan."""
super().__init__(hub, device) super().__init__(hub, device)
self._brightness: Optional[int] = None
self._light: Optional[int] = None self._light: Optional[int] = None
def _apply_state(self, state: dict):
self._light = state.get("light")
self._brightness = state.get("brightness")
@property
def supported_features(self) -> Optional[int]:
"""Flag supported features."""
features = 0
if self._device.supports_set_brightness():
features |= SUPPORT_BRIGHTNESS
return features
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return if light is currently on.""" """Return if light is currently on."""
return self._light == 1 return self._light == 1
def update(self): @property
"""Fetch assumed state of the light from the hub using API.""" def brightness(self) -> int:
state: dict = self._hub.bond.getDeviceState(self._device.device_id) """Return the brightness of this light between 1..255."""
self._light = state.get("light") brightness_value = (
round(self._brightness * 255 / 100) if self._brightness else None
)
return brightness_value
def turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light.""" """Turn on the light."""
self._hub.bond.turnLightOn(self._device.device_id) brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness:
await self._hub.bond.action(
self._device.device_id,
Action.set_brightness(round((brightness * 100) / 255)),
)
else:
await self._hub.bond.action(self._device.device_id, Action.turn_light_on())
def turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light.""" """Turn off the light."""
self._hub.bond.turnLightOff(self._device.device_id) await self._hub.bond.action(self._device.device_id, Action.turn_light_off())
class BondFireplace(BondEntity, LightEntity): class BondFireplace(BondEntity, LightEntity):
@ -82,6 +103,10 @@ class BondFireplace(BondEntity, LightEntity):
# Bond flame level, 0-100 # Bond flame level, 0-100
self._flame: Optional[int] = None self._flame: Optional[int] = None
def _apply_state(self, state: dict):
self._power = state.get("power")
self._flame = state.get("flame")
@property @property
def supported_features(self) -> Optional[int]: def supported_features(self) -> Optional[int]:
"""Flag brightness as supported feature to represent flame level.""" """Flag brightness as supported feature to represent flame level."""
@ -92,18 +117,18 @@ class BondFireplace(BondEntity, LightEntity):
"""Return True if power is on.""" """Return True if power is on."""
return self._power == 1 return self._power == 1
def turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the fireplace on.""" """Turn the fireplace on."""
self._hub.bond.turnOn(self._device.device_id)
brightness = kwargs.get(ATTR_BRIGHTNESS) brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness: if brightness:
flame = round((brightness * 100) / 255) flame = round((brightness * 100) / 255)
self._hub.bond.setFlame(self._device.device_id, flame) await self._hub.bond.action(self._device.device_id, Action.set_flame(flame))
else:
await self._hub.bond.action(self._device.device_id, Action.turn_on())
def turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fireplace off.""" """Turn the fireplace off."""
self._hub.bond.turnOff(self._device.device_id) await self._hub.bond.action(self._device.device_id, Action.turn_off())
@property @property
def brightness(self): def brightness(self):
@ -114,9 +139,3 @@ class BondFireplace(BondEntity, LightEntity):
def icon(self) -> Optional[str]: def icon(self) -> Optional[str]:
"""Show fireplace icon for the entity.""" """Show fireplace icon for the entity."""
return "mdi:fireplace" if self._power == 1 else "mdi:fireplace-off" return "mdi:fireplace" if self._power == 1 else "mdi:fireplace-off"
def update(self):
"""Fetch assumed state of the device from the hub using API."""
state: dict = self._hub.bond.getDeviceState(self._device.device_id)
self._power = state.get("power")
self._flame = state.get("flame")

View File

@ -3,10 +3,8 @@
"name": "Bond", "name": "Bond",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bond", "documentation": "https://www.home-assistant.io/integrations/bond",
"requirements": [ "requirements": ["bond-api==0.1.8"],
"bond-home==0.0.9" "zeroconf": ["_bond._tcp.local."],
], "codeowners": ["@prystupa"],
"codeowners": [ "quality_scale": "platinum"
"@prystupa"
]
} }

View File

@ -1,6 +1,13 @@
{ {
"config": { "config": {
"flow_title": "Bond: {bond_id} ({host})",
"step": { "step": {
"confirm": {
"description": "Do you want to set up {bond_id}?",
"data": {
"access_token": "[%key:common::config_flow::data::access_token%]"
}
},
"user": { "user": {
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
@ -12,6 +19,9 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
} }
} }
} }

View File

@ -1,13 +1,13 @@
"""Support for Bond generic devices.""" """Support for Bond generic devices."""
from typing import Any, Callable, List, Optional from typing import Any, Callable, List, Optional
from bond import DeviceTypes from bond_api import Action, DeviceType
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from ..switch import SwitchEntity
from .const import DOMAIN from .const import DOMAIN
from .entity import BondEntity from .entity import BondEntity
from .utils import BondDevice, BondHub from .utils import BondDevice, BondHub
@ -21,12 +21,10 @@ async def async_setup_entry(
"""Set up Bond generic devices.""" """Set up Bond generic devices."""
hub: BondHub = hass.data[DOMAIN][entry.entry_id] hub: BondHub = hass.data[DOMAIN][entry.entry_id]
devices = await hass.async_add_executor_job(hub.get_bond_devices)
switches = [ switches = [
BondSwitch(hub, device) BondSwitch(hub, device)
for device in devices for device in hub.devices
if device.type == DeviceTypes.GENERIC_DEVICE if DeviceType.is_generic(device.type)
] ]
async_add_entities(switches, True) async_add_entities(switches, True)
@ -41,20 +39,18 @@ class BondSwitch(BondEntity, SwitchEntity):
self._power: Optional[bool] = None self._power: Optional[bool] = None
def _apply_state(self, state: dict):
self._power = state.get("power")
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return True if power is on.""" """Return True if power is on."""
return self._power == 1 return self._power == 1
def turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on.""" """Turn the device on."""
self._hub.bond.turnOn(self._device.device_id) await self._hub.bond.action(self._device.device_id, Action.turn_on())
def turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off.""" """Turn the device off."""
self._hub.bond.turnOff(self._device.device_id) await self._hub.bond.action(self._device.device_id, Action.turn_off())
def update(self):
"""Fetch assumed state of the device from the hub using API."""
state: dict = self._hub.bond.getDeviceState(self._device.device_id)
self._power = state.get("power")

View File

@ -1,5 +1,8 @@
{ {
"config": { "config": {
"abort": {
"already_configured": "El dispositiu ja est\u00e0 configurat"
},
"error": { "error": {
"cannot_connect": "Ha fallat la connexi\u00f3", "cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",

View File

@ -0,0 +1,16 @@
{
"config": {
"abort": {
"already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakofigurovan\u00e9"
},
"flow_title": "Bond: {bond_id} ({host})",
"step": {
"confirm": {
"data": {
"access_token": "P\u0159\u00edstupov\u00fd token"
},
"description": "Chcete nastavit {bond_id} ?"
}
}
}
}

View File

@ -1,11 +1,21 @@
{ {
"config": { "config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": { "error": {
"cannot_connect": "Failed to connect", "cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication", "invalid_auth": "Invalid authentication",
"unknown": "Unexpected error" "unknown": "Unexpected error"
}, },
"flow_title": "Bond: {bond_id} ({host})",
"step": { "step": {
"confirm": {
"data": {
"access_token": "Access Token"
},
"description": "Do you want to set up {bond_id}?"
},
"user": { "user": {
"data": { "data": {
"access_token": "Access Token", "access_token": "Access Token",

View File

@ -1,11 +1,21 @@
{ {
"config": { "config": {
"abort": {
"already_configured": "El dispositivo ya est\u00e1 configurado"
},
"error": { "error": {
"cannot_connect": "No se pudo conectar", "cannot_connect": "No se pudo conectar",
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
"unknown": "Error inesperado" "unknown": "Error inesperado"
}, },
"flow_title": "Bond: {bond_id} ({host})",
"step": { "step": {
"confirm": {
"data": {
"access_token": "Token de acceso"
},
"description": "\u00bfQuieres configurar {bond_id}?"
},
"user": { "user": {
"data": { "data": {
"access_token": "Token de acceso", "access_token": "Token de acceso",

View File

@ -1,11 +1,21 @@
{ {
"config": { "config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
},
"error": { "error": {
"cannot_connect": "Impossibile connettersi", "cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida", "invalid_auth": "Autenticazione non valida",
"unknown": "Errore imprevisto" "unknown": "Errore imprevisto"
}, },
"flow_title": "Bond: {bond_id} ({host})",
"step": { "step": {
"confirm": {
"data": {
"access_token": "Token di accesso"
},
"description": "Vuoi configurare {bond_id}?"
},
"user": { "user": {
"data": { "data": {
"access_token": "Token di accesso", "access_token": "Token di accesso",

View File

@ -0,0 +1,17 @@
{
"config": {
"error": {
"cannot_connect": "Feeler beim verbannen",
"invalid_auth": "Ong\u00eblteg Authentifikatioun",
"unknown": "Onerwaarte Feeler"
},
"step": {
"user": {
"data": {
"access_token": "Acc\u00e8s jeton",
"host": "Host"
}
}
}
}
}

View File

@ -0,0 +1,27 @@
{
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert"
},
"error": {
"cannot_connect": "Tilkobling mislyktes.",
"invalid_auth": "Ugyldig godkjenning",
"unknown": "Uventet feil"
},
"flow_title": "Obligasjon: {bond_id} ( {host} )",
"step": {
"confirm": {
"data": {
"access_token": "Tilgangstoken"
},
"description": "Vil du konfigurere {bond_id}?"
},
"user": {
"data": {
"access_token": "Tilgangstoken",
"host": "Vert"
}
}
}
}
}

View File

@ -0,0 +1,17 @@
{
"config": {
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
"invalid_auth": "Niepoprawne uwierzytelnienie.",
"unknown": "Nieoczekiwany b\u0142\u0105d."
},
"step": {
"user": {
"data": {
"access_token": "Token dost\u0119pu",
"host": "Nazwa hosta lub adres IP"
}
}
}
}
}

View File

@ -1,11 +1,21 @@
{ {
"config": { "config": {
"abort": {
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
},
"error": { "error": {
"cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.",
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
}, },
"flow_title": "Bond {bond_id} ({host})",
"step": { "step": {
"confirm": {
"data": {
"access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430"
},
"description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {bond_id}?"
},
"user": { "user": {
"data": { "data": {
"access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430",

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