mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Merge pull request #38785 from home-assistant/rc
This commit is contained in:
commit
8eb6f29c53
45
.coveragerc
45
.coveragerc
@ -8,6 +8,10 @@ omit =
|
||||
homeassistant/scripts/*.py
|
||||
|
||||
# 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/actiontec/device_tracker.py
|
||||
homeassistant/components/acmeda/__init__.py
|
||||
@ -28,10 +32,6 @@ omit =
|
||||
homeassistant/components/agent_dvr/camera.py
|
||||
homeassistant/components/agent_dvr/const.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/air_quality.py
|
||||
homeassistant/components/airvisual/sensor.py
|
||||
@ -69,6 +69,9 @@ omit =
|
||||
homeassistant/components/avion/light.py
|
||||
homeassistant/components/avri/const.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/baidu/tts.py
|
||||
homeassistant/components/beewi_smartclim/sensor.py
|
||||
@ -139,6 +142,10 @@ omit =
|
||||
homeassistant/components/comfoconnect/*
|
||||
homeassistant/components/concord232/alarm_control_panel.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/climate.py
|
||||
homeassistant/components/coolmaster/const.py
|
||||
@ -164,6 +171,8 @@ omit =
|
||||
homeassistant/components/devolo_home_control/binary_sensor.py
|
||||
homeassistant/components/devolo_home_control/const.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/subscriber.py
|
||||
homeassistant/components/devolo_home_control/switch.py
|
||||
@ -254,6 +263,13 @@ omit =
|
||||
homeassistant/components/fibaro/*
|
||||
homeassistant/components/filesize/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/fixer/sensor.py
|
||||
homeassistant/components/fleetgo/device_tracker.py
|
||||
@ -337,7 +353,8 @@ omit =
|
||||
homeassistant/components/hisense_aehw4a1/*
|
||||
homeassistant/components/hitron_coda/device_tracker.py
|
||||
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/homematic/*
|
||||
homeassistant/components/homematic/climate.py
|
||||
@ -443,8 +460,6 @@ omit =
|
||||
homeassistant/components/lightwave/*
|
||||
homeassistant/components/limitlessled/light.py
|
||||
homeassistant/components/linksys_smart/device_tracker.py
|
||||
homeassistant/components/linky/__init__.py
|
||||
homeassistant/components/linky/sensor.py
|
||||
homeassistant/components/linode/*
|
||||
homeassistant/components/linux_battery/sensor.py
|
||||
homeassistant/components/lirc/*
|
||||
@ -537,6 +552,10 @@ omit =
|
||||
homeassistant/components/netatmo/camera.py
|
||||
homeassistant/components/netatmo/climate.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/webhook.py
|
||||
homeassistant/components/netdata/sensor.py
|
||||
@ -605,6 +624,9 @@ omit =
|
||||
homeassistant/components/orvibo/switch.py
|
||||
homeassistant/components/osramlightify/light.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_viera/media_player.py
|
||||
homeassistant/components/pandora/media_player.py
|
||||
@ -617,6 +639,7 @@ omit =
|
||||
homeassistant/components/picotts/tts.py
|
||||
homeassistant/components/piglow/light.py
|
||||
homeassistant/components/pilight/*
|
||||
homeassistant/components/ping/const.py
|
||||
homeassistant/components/ping/binary_sensor.py
|
||||
homeassistant/components/ping/device_tracker.py
|
||||
homeassistant/components/pioneer/media_player.py
|
||||
@ -679,7 +702,6 @@ omit =
|
||||
homeassistant/components/rest/binary_sensor.py
|
||||
homeassistant/components/rest/notify.py
|
||||
homeassistant/components/rest/switch.py
|
||||
homeassistant/components/rfxtrx/*
|
||||
homeassistant/components/ring/camera.py
|
||||
homeassistant/components/ripple/sensor.py
|
||||
homeassistant/components/rocketchat/notify.py
|
||||
@ -729,7 +751,7 @@ omit =
|
||||
homeassistant/components/simplisafe/lock.py
|
||||
homeassistant/components/simulated/sensor.py
|
||||
homeassistant/components/sisyphus/*
|
||||
homeassistant/components/sky_hub/device_tracker.py
|
||||
homeassistant/components/sky_hub/*
|
||||
homeassistant/components/skybeacon/sensor.py
|
||||
homeassistant/components/skybell/*
|
||||
homeassistant/components/slack/notify.py
|
||||
@ -909,6 +931,7 @@ omit =
|
||||
homeassistant/components/vlc/media_player.py
|
||||
homeassistant/components/vlc_telnet/media_player.py
|
||||
homeassistant/components/volkszaehler/sensor.py
|
||||
homeassistant/components/volumio/__init__.py
|
||||
homeassistant/components/volumio/media_player.py
|
||||
homeassistant/components/volvooncall/*
|
||||
homeassistant/components/w800rf32/*
|
||||
@ -923,6 +946,9 @@ omit =
|
||||
homeassistant/components/wiffi/*
|
||||
homeassistant/components/wink/*
|
||||
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/worxlandroid/sensor.py
|
||||
homeassistant/components/x10/light.py
|
||||
@ -955,7 +981,6 @@ omit =
|
||||
homeassistant/components/yale_smart_alarm/alarm_control_panel.py
|
||||
homeassistant/components/yamaha_musiccast/media_player.py
|
||||
homeassistant/components/yandex_transport/*
|
||||
homeassistant/components/yeelight/*
|
||||
homeassistant/components/yeelightsunflower/light.py
|
||||
homeassistant/components/yi/camera.py
|
||||
homeassistant/components/zabbix/*
|
||||
|
28
.github/workflows/ci.yaml
vendored
28
.github/workflows/ci.yaml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v2.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore base Python virtual environment
|
||||
@ -75,7 +75,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v2.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -119,7 +119,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v2.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -163,7 +163,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v2.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -229,7 +229,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v2.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -276,7 +276,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v2.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -323,7 +323,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v2.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -367,7 +367,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v2.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -414,7 +414,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v2.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -469,7 +469,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v2.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -516,7 +516,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v2.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -548,7 +548,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v2.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -737,7 +737,7 @@ jobs:
|
||||
-p no:sugar \
|
||||
tests
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@2.1.0
|
||||
uses: actions/upload-artifact@v2.1.3
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-group${{ matrix.group }}
|
||||
path: .coverage
|
||||
@ -781,4 +781,4 @@ jobs:
|
||||
coverage report --fail-under=94
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1.0.10
|
||||
uses: codecov/codecov-action@v1.0.12
|
||||
|
@ -12,6 +12,7 @@ addons:
|
||||
- libavfilter-dev
|
||||
sources:
|
||||
- sourceline: ppa:savoury1/ffmpeg4
|
||||
- sourceline: ppa:savoury1/multimedia
|
||||
|
||||
python:
|
||||
- "3.7.1"
|
||||
|
18
CODEOWNERS
18
CODEOWNERS
@ -14,6 +14,7 @@ homeassistant/scripts/check_config.py @kellerza
|
||||
|
||||
# Integrations
|
||||
homeassistant/components/abode/* @shred86
|
||||
homeassistant/components/accuweather/* @bieniu
|
||||
homeassistant/components/acmeda/* @atmurray
|
||||
homeassistant/components/adguard/* @frenck
|
||||
homeassistant/components/agent_dvr/* @ispysoftware
|
||||
@ -48,6 +49,7 @@ homeassistant/components/avri/* @timvancann
|
||||
homeassistant/components/awair/* @ahayworth @danielsjf
|
||||
homeassistant/components/aws/* @awarecan
|
||||
homeassistant/components/axis/* @Kane610
|
||||
homeassistant/components/azure_devops/* @timmo001
|
||||
homeassistant/components/azure_event_hub/* @eavanvalkenburg
|
||||
homeassistant/components/azure_service_bus/* @hfurubotten
|
||||
homeassistant/components/beewi_smartclim/* @alemuro
|
||||
@ -77,6 +79,7 @@ homeassistant/components/cloudflare/* @ludeeus
|
||||
homeassistant/components/comfoconnect/* @michaelarnauts
|
||||
homeassistant/components/config/* @home-assistant/core
|
||||
homeassistant/components/configurator/* @home-assistant/core
|
||||
homeassistant/components/control4/* @lawtancool
|
||||
homeassistant/components/conversation/* @home-assistant/core
|
||||
homeassistant/components/coolmaster/* @OnFreund
|
||||
homeassistant/components/coronavirus/* @home_assistant/core
|
||||
@ -110,7 +113,7 @@ homeassistant/components/edl21/* @mtdcr
|
||||
homeassistant/components/egardia/* @jeroenterheerdt
|
||||
homeassistant/components/eight_sleep/* @mezz64
|
||||
homeassistant/components/elgato/* @frenck
|
||||
homeassistant/components/elkm1/* @bdraco
|
||||
homeassistant/components/elkm1/* @gwww @bdraco
|
||||
homeassistant/components/elv/* @majuss
|
||||
homeassistant/components/emby/* @mezz64
|
||||
homeassistant/components/emoncms/* @borpin
|
||||
@ -128,6 +131,7 @@ homeassistant/components/ezviz/* @baqs
|
||||
homeassistant/components/fastdotcom/* @rohankapoorcom
|
||||
homeassistant/components/file/* @fabaff
|
||||
homeassistant/components/filter/* @dgomes
|
||||
homeassistant/components/firmata/* @DaAwesomeP
|
||||
homeassistant/components/fixer/* @fabaff
|
||||
homeassistant/components/flick_electric/* @ZephireNZ
|
||||
homeassistant/components/flock/* @fabaff
|
||||
@ -168,6 +172,7 @@ homeassistant/components/hikvisioncam/* @fbradyirl
|
||||
homeassistant/components/hisense_aehw4a1/* @bannhead
|
||||
homeassistant/components/history/* @home-assistant/core
|
||||
homeassistant/components/hive/* @Rendili @KJonline
|
||||
homeassistant/components/hlk_sw16/* @jameshilliard
|
||||
homeassistant/components/home_connect/* @DavidMStraub
|
||||
homeassistant/components/homeassistant/* @home-assistant/core
|
||||
homeassistant/components/homekit/* @bdraco
|
||||
@ -221,7 +226,6 @@ homeassistant/components/lametric/* @robbiet480
|
||||
homeassistant/components/launch_library/* @ludeeus
|
||||
homeassistant/components/lcn/* @alengwenus
|
||||
homeassistant/components/life360/* @pnbruckner
|
||||
homeassistant/components/linky/* @Quentame
|
||||
homeassistant/components/linux_battery/* @fabaff
|
||||
homeassistant/components/local_ip/* @issacg
|
||||
homeassistant/components/logger/* @home-assistant/core
|
||||
@ -239,7 +243,7 @@ homeassistant/components/mediaroom/* @dgomes
|
||||
homeassistant/components/melcloud/* @vilppuvuorinen
|
||||
homeassistant/components/melissa/* @kennedyshead
|
||||
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/metoffice/* @MrHarcombe
|
||||
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
|
||||
@ -297,6 +301,7 @@ homeassistant/components/openweathermap/* @fabaff
|
||||
homeassistant/components/opnsense/* @mtreinish
|
||||
homeassistant/components/orangepi_gpio/* @pascallj
|
||||
homeassistant/components/oru/* @bvlaicu
|
||||
homeassistant/components/ovo_energy/* @timmo001
|
||||
homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare
|
||||
homeassistant/components/panasonic_viera/* @joogps
|
||||
homeassistant/components/panel_custom/* @home-assistant/frontend
|
||||
@ -362,6 +367,7 @@ homeassistant/components/signal_messenger/* @bbernhard
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/sinch/* @bendikrb
|
||||
homeassistant/components/sisyphus/* @jkeljo
|
||||
homeassistant/components/sky_hub/* @rogerselwyn
|
||||
homeassistant/components/slide/* @ualex73
|
||||
homeassistant/components/sma/* @kellerza
|
||||
homeassistant/components/smappee/* @bsmappee
|
||||
@ -449,6 +455,7 @@ homeassistant/components/vilfo/* @ManneW
|
||||
homeassistant/components/vivotek/* @HarlemSquirrel
|
||||
homeassistant/components/vizio/* @raman325
|
||||
homeassistant/components/vlc_telnet/* @rodripf
|
||||
homeassistant/components/volumio/* @OnFreund
|
||||
homeassistant/components/waqi/* @andrey-git
|
||||
homeassistant/components/watson_tts/* @rutkai
|
||||
homeassistant/components/weather/* @fabaff
|
||||
@ -457,16 +464,17 @@ homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wiffi/* @mampfes
|
||||
homeassistant/components/withings/* @vangorra
|
||||
homeassistant/components/wled/* @frenck
|
||||
homeassistant/components/wolflink/* @adamkrol93
|
||||
homeassistant/components/workday/* @fabaff
|
||||
homeassistant/components/worldclock/* @fabaff
|
||||
homeassistant/components/xbox_live/* @MartinHjelmare
|
||||
homeassistant/components/xfinity/* @cisasteelersfan
|
||||
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/xmpp/* @fabaff @flowolf
|
||||
homeassistant/components/yamaha_musiccast/* @jalmeroth
|
||||
homeassistant/components/yandex_transport/* @rishatik92
|
||||
homeassistant/components/yandex_transport/* @rishatik92 @devbis
|
||||
homeassistant/components/yeelight/* @rytilahti @zewelor
|
||||
homeassistant/components/yeelightsunflower/* @lindsaymarkward
|
||||
homeassistant/components/yessssms/* @flowolf
|
||||
|
@ -29,12 +29,31 @@ jobs:
|
||||
- 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'
|
||||
builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev'
|
||||
builderPip: 'Cython;numpy'
|
||||
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'
|
||||
wheelsRequirementDiff: 'requirements_diff.txt'
|
||||
wheelsConstraint: 'homeassistant/package_constraints.txt'
|
||||
jobName: 'Wheels_Integrations'
|
||||
preBuild:
|
||||
- script: |
|
||||
cp requirements_all.txt requirements_wheels.txt
|
||||
|
10
build.json
10
build.json
@ -1,11 +1,11 @@
|
||||
{
|
||||
"image": "homeassistant/{arch}-homeassistant",
|
||||
"build_from": {
|
||||
"aarch64": "homeassistant/aarch64-homeassistant-base:8.0.0",
|
||||
"armhf": "homeassistant/armhf-homeassistant-base:8.0.0",
|
||||
"armv7": "homeassistant/armv7-homeassistant-base:8.0.0",
|
||||
"amd64": "homeassistant/amd64-homeassistant-base:8.0.0",
|
||||
"i386": "homeassistant/i386-homeassistant-base:8.0.0"
|
||||
"aarch64": "homeassistant/aarch64-homeassistant-base:8.2.1",
|
||||
"armhf": "homeassistant/armhf-homeassistant-base:8.2.1",
|
||||
"armv7": "homeassistant/armv7-homeassistant-base:8.2.1",
|
||||
"amd64": "homeassistant/amd64-homeassistant-base:8.2.1",
|
||||
"i386": "homeassistant/i386-homeassistant-base:8.2.1"
|
||||
},
|
||||
"labels": {
|
||||
"io.hass.type": "core"
|
||||
|
@ -7,7 +7,7 @@ from homeassistant.util.async_ import protect_loop
|
||||
def enable() -> None:
|
||||
"""Enable the detection of I/O in the 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.
|
||||
# Prevent files being opened inside the event loop
|
||||
|
@ -6,10 +6,10 @@ import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from time import monotonic
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
|
||||
|
||||
from async_timeout import timeout
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
||||
@ -44,6 +44,11 @@ DATA_LOGGING = "logging"
|
||||
|
||||
LOG_SLOW_STARTUP_INTERVAL = 60
|
||||
|
||||
STAGE_1_TIMEOUT = 120
|
||||
STAGE_2_TIMEOUT = 300
|
||||
WRAP_UP_TIMEOUT = 300
|
||||
COOLDOWN_TIME = 60
|
||||
|
||||
DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"}
|
||||
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
|
||||
LOGGING_INTEGRATIONS = {
|
||||
@ -136,7 +141,7 @@ async def async_setup_hass(
|
||||
hass.async_track_tasks()
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
|
||||
with contextlib.suppress(asyncio.TimeoutError):
|
||||
async with timeout(10):
|
||||
async with hass.timeout.async_timeout(10):
|
||||
await hass.async_block_till_done()
|
||||
|
||||
safe_mode = True
|
||||
@ -304,6 +309,12 @@ def async_enable_logging(
|
||||
"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
|
||||
if log_file is None:
|
||||
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
|
||||
|
||||
# Kick off loading the registries. They don't need to be awaited.
|
||||
asyncio.gather(
|
||||
hass.helpers.device_registry.async_get_registry(),
|
||||
hass.helpers.entity_registry.async_get_registry(),
|
||||
hass.helpers.area_registry.async_get_registry(),
|
||||
)
|
||||
asyncio.create_task(hass.helpers.device_registry.async_get_registry())
|
||||
asyncio.create_task(hass.helpers.entity_registry.async_get_registry())
|
||||
asyncio.create_task(hass.helpers.area_registry.async_get_registry())
|
||||
|
||||
# Start setup
|
||||
if 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
|
||||
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
|
||||
|
||||
if 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
|
||||
_LOGGER.debug("Waiting for startup to wrap up")
|
||||
await hass.async_block_till_done()
|
||||
try:
|
||||
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
|
||||
await hass.async_block_till_done()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.warning("Setup timed out for bootstrap - moving forward")
|
||||
|
132
homeassistant/components/accuweather/__init__.py
Normal file
132
homeassistant/components/accuweather/__init__.py
Normal 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}}
|
112
homeassistant/components/accuweather/config_flow.py
Normal file
112
homeassistant/components/accuweather/config_flow.py
Normal 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
|
||||
}
|
||||
),
|
||||
)
|
279
homeassistant/components/accuweather/const.py
Normal file
279
homeassistant/components/accuweather/const.py
Normal 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,
|
||||
},
|
||||
}
|
8
homeassistant/components/accuweather/manifest.json
Normal file
8
homeassistant/components/accuweather/manifest.json
Normal 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
|
||||
}
|
189
homeassistant/components/accuweather/sensor.py
Normal file
189
homeassistant/components/accuweather/sensor.py
Normal 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()
|
35
homeassistant/components/accuweather/strings.json
Normal file
35
homeassistant/components/accuweather/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
homeassistant/components/accuweather/strings.sensor.json
Normal file
9
homeassistant/components/accuweather/strings.sensor.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"state": {
|
||||
"accuweather__pressure_tendency": {
|
||||
"steady": "Steady",
|
||||
"rising": "Rising",
|
||||
"falling": "Falling"
|
||||
}
|
||||
}
|
||||
}
|
35
homeassistant/components/accuweather/translations/ca.json
Normal file
35
homeassistant/components/accuweather/translations/ca.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
homeassistant/components/accuweather/translations/en.json
Normal file
35
homeassistant/components/accuweather/translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
homeassistant/components/accuweather/translations/es.json
Normal file
35
homeassistant/components/accuweather/translations/es.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
homeassistant/components/accuweather/translations/it.json
Normal file
35
homeassistant/components/accuweather/translations/it.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
homeassistant/components/accuweather/translations/lb.json
Normal file
35
homeassistant/components/accuweather/translations/lb.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
homeassistant/components/accuweather/translations/no.json
Normal file
35
homeassistant/components/accuweather/translations/no.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
homeassistant/components/accuweather/translations/pl.json
Normal file
35
homeassistant/components/accuweather/translations/pl.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/accuweather/translations/pt.json
Normal file
21
homeassistant/components/accuweather/translations/pt.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"forecast": "Previs\u00e3o meteorol\u00f3gica"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
homeassistant/components/accuweather/translations/ru.json
Normal file
35
homeassistant/components/accuweather/translations/ru.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"state": {
|
||||
"accuweather__pressure_tendency": {
|
||||
"falling": "Klesaj\u00edc\u00ed",
|
||||
"rising": "Roustouc\u00ed",
|
||||
"steady": "St\u00e1l\u00fd"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"state": {
|
||||
"accuweather__pressure_tendency": {
|
||||
"falling": "Falling",
|
||||
"rising": "Rising",
|
||||
"steady": "Steady"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"state": {
|
||||
"accuweather__pressure_tendency": {
|
||||
"falling": "Cayendo",
|
||||
"rising": "Subiendo",
|
||||
"steady": "Estable"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"state": {
|
||||
"accuweather__pressure_tendency": {
|
||||
"falling": "Diminuzione",
|
||||
"rising": "Aumento",
|
||||
"steady": "Stabile"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"state": {
|
||||
"accuweather__pressure_tendency": {
|
||||
"falling": "Fallende",
|
||||
"rising": "Stiger",
|
||||
"steady": "Jevn"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"state": {
|
||||
"accuweather__pressure_tendency": {
|
||||
"falling": "spada",
|
||||
"rising": "ro\u015bnie",
|
||||
"steady": "bez zmian"
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
26
homeassistant/components/accuweather/translations/uk.json
Normal file
26
homeassistant/components/accuweather/translations/uk.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
195
homeassistant/components/accuweather/weather.py
Normal file
195
homeassistant/components/accuweather/weather.py
Normal 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)
|
@ -17,8 +17,10 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Vert",
|
||||
"password": "Passord",
|
||||
"port": "",
|
||||
"ssl": "AdGuard Hjem bruker et SSL-sertifikat",
|
||||
"username": "Brukernavn",
|
||||
"verify_ssl": "AdGuard Home bruker et riktig sertifikat"
|
||||
},
|
||||
"description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll."
|
||||
|
@ -3,7 +3,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"username": "E-pasts"
|
||||
"port": "Poort"
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Vert",
|
||||
"port": "Port"
|
||||
"port": ""
|
||||
},
|
||||
"title": "Konfigurere Agent DVR"
|
||||
}
|
||||
|
@ -18,7 +18,9 @@ from .const import (
|
||||
ATTR_API_PM25,
|
||||
ATTR_API_PM25_LIMIT,
|
||||
ATTR_API_PM25_PERCENT,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
)
|
||||
|
||||
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_PERCENT = f"{ATTR_PM_10}_percent_of_limit"
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""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]
|
||||
|
||||
async_add_entities(
|
||||
[AirlyAirQuality(coordinator, name, config_entry.unique_id)], False
|
||||
)
|
||||
async_add_entities([AirlyAirQuality(coordinator, name)], False)
|
||||
|
||||
|
||||
def round_state(func):
|
||||
@ -58,11 +60,10 @@ def round_state(func):
|
||||
class AirlyAirQuality(AirQualityEntity):
|
||||
"""Define an Airly air quality."""
|
||||
|
||||
def __init__(self, coordinator, name, unique_id):
|
||||
def __init__(self, coordinator, name):
|
||||
"""Initialize."""
|
||||
self.coordinator = coordinator
|
||||
self._name = name
|
||||
self._unique_id = unique_id
|
||||
self._icon = "mdi:blur"
|
||||
|
||||
@property
|
||||
@ -106,7 +107,19 @@ class AirlyAirQuality(AirQualityEntity):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""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
|
||||
def available(self):
|
||||
|
@ -15,5 +15,6 @@ ATTR_API_PRESSURE = "PRESSURE"
|
||||
ATTR_API_TEMPERATURE = "TEMPERATURE"
|
||||
DEFAULT_NAME = "Airly"
|
||||
DOMAIN = "airly"
|
||||
MANUFACTURER = "Airly sp. z o.o."
|
||||
MAX_REQUESTS_PER_DAY = 100
|
||||
NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet."
|
||||
|
@ -4,5 +4,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airly",
|
||||
"codeowners": ["@bieniu"],
|
||||
"requirements": ["airly==0.0.2"],
|
||||
"config_flow": true
|
||||
"config_flow": true,
|
||||
"quality_scale": "platinum"
|
||||
}
|
||||
|
@ -18,7 +18,9 @@ from .const import (
|
||||
ATTR_API_PM1,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_TEMPERATURE,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Data provided by Airly"
|
||||
@ -27,6 +29,8 @@ ATTR_ICON = "icon"
|
||||
ATTR_LABEL = "label"
|
||||
ATTR_UNIT = "unit"
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
SENSOR_TYPES = {
|
||||
ATTR_API_PM1: {
|
||||
ATTR_DEVICE_CLASS: None,
|
||||
@ -63,8 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
|
||||
sensors = []
|
||||
for sensor in SENSOR_TYPES:
|
||||
unique_id = f"{config_entry.unique_id}-{sensor.lower()}"
|
||||
sensors.append(AirlySensor(coordinator, name, sensor, unique_id))
|
||||
sensors.append(AirlySensor(coordinator, name, sensor))
|
||||
|
||||
async_add_entities(sensors, False)
|
||||
|
||||
@ -72,11 +75,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
class AirlySensor(Entity):
|
||||
"""Define an Airly sensor."""
|
||||
|
||||
def __init__(self, coordinator, name, kind, unique_id):
|
||||
def __init__(self, coordinator, name, kind):
|
||||
"""Initialize."""
|
||||
self.coordinator = coordinator
|
||||
self._name = name
|
||||
self._unique_id = unique_id
|
||||
self.kind = kind
|
||||
self._device_class = None
|
||||
self._state = None
|
||||
@ -123,7 +125,19 @@ class AirlySensor(Entity):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""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
|
||||
def unit_of_measurement(self):
|
||||
|
@ -10,7 +10,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "Clave API de Airly",
|
||||
"api_key": "Clave API",
|
||||
"latitude": "Latitud",
|
||||
"longitude": "Longitud",
|
||||
"name": "Nombre de la integraci\u00f3n"
|
||||
|
@ -1,25 +1,39 @@
|
||||
{
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"arm_away": "Aktivovat {entity_name} v re\u017eimu mimo domov",
|
||||
"arm_home": "Aktivovat {entity_name} v re\u017eimu doma",
|
||||
"arm_night": "Aktivovat {entity_name} v re\u017eimu noc",
|
||||
"disarm": "Deaktivovat {entity_name}",
|
||||
"arm_away": "Aktivovat {entity_name} v re\u017eimu nep\u0159\u00edtomnost",
|
||||
"arm_home": "Aktivovat {entity_name} v re\u017eimu domov",
|
||||
"arm_night": "Aktivovat {entity_name} v no\u010dn\u00edm re\u017eimu",
|
||||
"disarm": "Odbezpe\u010dit {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": {
|
||||
"_": {
|
||||
"armed": "Aktivn\u00ed",
|
||||
"armed_away": "Aktivn\u00ed re\u017eim mimo domov",
|
||||
"armed_custom_bypass": "Aktivn\u00ed u\u017eivatelsk\u00fdm obejit\u00edm",
|
||||
"armed_home": "Aktivn\u00ed re\u017eim doma",
|
||||
"armed_night": "Aktivn\u00ed no\u010dn\u00ed re\u017eim",
|
||||
"arming": "Aktivov\u00e1n\u00ed",
|
||||
"disarmed": "Neaktivn\u00ed",
|
||||
"disarming": "Deaktivov\u00e1n\u00ed",
|
||||
"pending": "Nadch\u00e1zej\u00edc\u00ed",
|
||||
"triggered": "Spu\u0161t\u011bno"
|
||||
"armed": "Zabezpe\u010deno",
|
||||
"armed_away": "Re\u017eim nep\u0159\u00edtomnost",
|
||||
"armed_custom_bypass": "Zabezpe\u010deno u\u017eivatelsk\u00fdm obejit\u00edm",
|
||||
"armed_home": "Re\u017eim domov",
|
||||
"armed_night": "No\u010dn\u00ed re\u017eim",
|
||||
"arming": "Zabezpe\u010dov\u00e1n\u00ed",
|
||||
"disarmed": "Nezabezpe\u010deno",
|
||||
"disarming": "Odbezpe\u010dov\u00e1n\u00ed",
|
||||
"pending": "\u010cekaj\u00edc\u00ed",
|
||||
"triggered": "Spu\u0161t\u011bn"
|
||||
}
|
||||
},
|
||||
"title": "Ovl\u00e1dac\u00ed panel alarmu"
|
||||
|
@ -84,6 +84,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
|
||||
self._name = "Alarm Panel"
|
||||
self._state = None
|
||||
self._ac_power = None
|
||||
self._alarm_event_occurred = None
|
||||
self._backlight_on = None
|
||||
self._battery_low = None
|
||||
self._check_zone = None
|
||||
@ -117,6 +118,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
|
||||
self._ac_power = message.ac_power
|
||||
self._alarm_event_occurred = message.alarm_event_occurred
|
||||
self._backlight_on = message.backlight_on
|
||||
self._battery_low = message.battery_low
|
||||
self._check_zone = message.check_zone
|
||||
@ -163,6 +165,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
"ac_power": self._ac_power,
|
||||
"alarm_event_occurred": self._alarm_event_occurred,
|
||||
"backlight_on": self._backlight_on,
|
||||
"battery_low": self._battery_low,
|
||||
"check_zone": self._check_zone,
|
||||
|
@ -293,7 +293,7 @@ async def async_setup_entry(hass, config_entry):
|
||||
Client(
|
||||
config_entry.data[CONF_API_KEY],
|
||||
config_entry.data[CONF_APP_KEY],
|
||||
session,
|
||||
session=session,
|
||||
),
|
||||
)
|
||||
hass.loop.create_task(ambient.ws_connect())
|
||||
|
@ -43,7 +43,9 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
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:
|
||||
devices = await client.api.get_devices()
|
||||
|
@ -3,6 +3,6 @@
|
||||
"name": "Ambient Weather Station",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ambient_station",
|
||||
"requirements": ["aioambient==1.1.1"],
|
||||
"requirements": ["aioambient==1.2.1"],
|
||||
"codeowners": ["@bachya"]
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import logging
|
||||
import os
|
||||
|
||||
from adb_shell.auth.keygen import keygen
|
||||
from adb_shell.auth.sign_pythonrsa import PythonRSASigner
|
||||
from adb_shell.exceptions import (
|
||||
AdbTimeoutError,
|
||||
InvalidChecksumError,
|
||||
@ -14,6 +13,7 @@ from adb_shell.exceptions import (
|
||||
TcpTimeoutException,
|
||||
)
|
||||
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.exceptions import LockNotAcquiredException
|
||||
from androidtv.setup_async import setup
|
||||
@ -40,6 +40,7 @@ from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
STATE_PAUSED,
|
||||
@ -175,9 +176,7 @@ def setup_androidtv(hass, config):
|
||||
keygen(adbkey)
|
||||
|
||||
# Load the ADB key
|
||||
with open(adbkey) as priv_key:
|
||||
priv = priv_key.read()
|
||||
signer = PythonRSASigner("", priv)
|
||||
signer = ADBPythonSync.load_adbkey(adbkey)
|
||||
adb_log = f"using Python ADB implementation with adbkey='{adbkey}'"
|
||||
|
||||
else:
|
||||
@ -230,6 +229,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
)
|
||||
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 = [
|
||||
aftv,
|
||||
config[CONF_NAME],
|
||||
|
@ -11,6 +11,10 @@
|
||||
"description": "Vil du legge Arcam FMJ p\u00e5 ` {host} ` til Home Assistant? "
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Vert",
|
||||
"port": ""
|
||||
},
|
||||
"description": "Vennligst skriv inn vertsnavnet eller IP-adressen til enheten."
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -12,7 +12,7 @@
|
||||
"data": {
|
||||
"email": "E-post (valgfritt)",
|
||||
"host": "Vert",
|
||||
"port": "Port "
|
||||
"port": ""
|
||||
},
|
||||
"title": "Koble til enheten"
|
||||
}
|
||||
|
@ -23,7 +23,13 @@ from homeassistant.const import (
|
||||
SERVICE_TURN_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.helpers import condition, extract_domain_configs
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -61,6 +67,7 @@ CONF_TRIGGER = "trigger"
|
||||
CONF_CONDITION_TYPE = "condition_type"
|
||||
CONF_INITIAL_STATE = "initial_state"
|
||||
CONF_SKIP_CONDITION = "skip_condition"
|
||||
CONF_STOP_ACTIONS = "stop_actions"
|
||||
|
||||
CONDITION_USE_TRIGGER_VALUES = "use_trigger_values"
|
||||
CONDITION_TYPE_AND = "and"
|
||||
@ -69,6 +76,7 @@ CONDITION_TYPE_OR = "or"
|
||||
|
||||
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
||||
DEFAULT_INITIAL_STATE = True
|
||||
DEFAULT_STOP_ACTIONS = True
|
||||
|
||||
EVENT_AUTOMATION_RELOADED = "automation_reloaded"
|
||||
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_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):
|
||||
"""Remove all automations and load new ones from config."""
|
||||
@ -255,11 +267,13 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
self._async_detach_triggers = None
|
||||
self._cond_func = cond_func
|
||||
self.action_script = action_script
|
||||
self.action_script.change_listener = self.async_write_ha_state
|
||||
self._last_triggered = None
|
||||
self._initial_state = initial_state
|
||||
self._is_enabled = False
|
||||
self._referenced_entities: Optional[Set[str]] = None
|
||||
self._referenced_devices: Optional[Set[str]] = None
|
||||
self._logger = _LOGGER
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -282,11 +296,10 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
attrs = {
|
||||
ATTR_LAST_TRIGGERED: self._last_triggered,
|
||||
ATTR_MODE: self.action_script.script_mode,
|
||||
ATTR_CUR: self.action_script.runs,
|
||||
}
|
||||
if self.action_script.supports_max:
|
||||
attrs[ATTR_MAX] = self.action_script.max_runs
|
||||
if self.is_on:
|
||||
attrs[ATTR_CUR] = self.action_script.runs
|
||||
return attrs
|
||||
|
||||
@property
|
||||
@ -337,13 +350,18 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
"""Startup with initial state or previous state."""
|
||||
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()
|
||||
if state:
|
||||
enable_automation = state.state == STATE_ON
|
||||
last_triggered = state.attributes.get("last_triggered")
|
||||
if last_triggered is not None:
|
||||
self._last_triggered = parse_datetime(last_triggered)
|
||||
_LOGGER.debug(
|
||||
self._logger.debug(
|
||||
"Loaded automation %s with state %s from state "
|
||||
" storage last state %s",
|
||||
self.entity_id,
|
||||
@ -352,7 +370,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
)
|
||||
else:
|
||||
enable_automation = DEFAULT_INITIAL_STATE
|
||||
_LOGGER.debug(
|
||||
self._logger.debug(
|
||||
"Automation %s not in state storage, state %s from default is used",
|
||||
self.entity_id,
|
||||
enable_automation,
|
||||
@ -360,7 +378,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
|
||||
if self._initial_state is not None:
|
||||
enable_automation = self._initial_state
|
||||
_LOGGER.debug(
|
||||
self._logger.debug(
|
||||
"Automation %s initial state %s overridden from "
|
||||
"config initial_state",
|
||||
self.entity_id,
|
||||
@ -376,7 +394,10 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self.async_disable()
|
||||
if CONF_STOP_ACTIONS in kwargs:
|
||||
await self.async_disable(kwargs[CONF_STOP_ACTIONS])
|
||||
else:
|
||||
await self.async_disable()
|
||||
|
||||
async def async_trigger(self, variables, skip_condition=False, context=None):
|
||||
"""Trigger automation.
|
||||
@ -403,12 +424,12 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
context=trigger_context,
|
||||
)
|
||||
|
||||
_LOGGER.info("Executing %s", self._name)
|
||||
self._logger.info("Executing %s", self._name)
|
||||
|
||||
try:
|
||||
await self.action_script.async_run(variables, trigger_context)
|
||||
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):
|
||||
"""Remove listeners when removing automation from Home Assistant."""
|
||||
@ -444,9 +465,9 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_disable(self):
|
||||
async def async_disable(self, stop_actions=DEFAULT_STOP_ACTIONS):
|
||||
"""Disable the automation entity."""
|
||||
if not self._is_enabled:
|
||||
if not self._is_enabled and not self.action_script.runs:
|
||||
return
|
||||
|
||||
self._is_enabled = False
|
||||
@ -455,7 +476,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
self._async_detach_triggers()
|
||||
self._async_detach_triggers = None
|
||||
|
||||
await self.action_script.async_stop()
|
||||
if stop_actions:
|
||||
await self.action_script.async_stop()
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@ -478,13 +500,13 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
results = await asyncio.gather(*triggers)
|
||||
|
||||
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]
|
||||
if not removes:
|
||||
return None
|
||||
|
||||
_LOGGER.info("Initialized trigger %s", self._name)
|
||||
self._logger.info("Initialized trigger %s", self._name)
|
||||
|
||||
@callback
|
||||
def remove_triggers():
|
||||
|
@ -12,6 +12,9 @@ turn_off:
|
||||
entity_id:
|
||||
description: Name of the automation to turn off.
|
||||
example: "automation.notify_home"
|
||||
stop_actions:
|
||||
description: Stop currently running actions (defaults to true).
|
||||
example: false
|
||||
|
||||
toggle:
|
||||
description: Toggle an automation.
|
||||
@ -27,7 +30,7 @@ trigger:
|
||||
description: Name of the automation to trigger.
|
||||
example: "automation.notify_home"
|
||||
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
|
||||
|
||||
reload:
|
||||
|
@ -73,16 +73,13 @@ async def async_attach_trigger(
|
||||
|
||||
from_s = event.data.get("old_state")
|
||||
to_s = event.data.get("new_state")
|
||||
old_state = getattr(from_s, "state", None)
|
||||
new_state = getattr(to_s, "state", None)
|
||||
|
||||
if (
|
||||
(from_s is not None and not match_from_state(from_s.state))
|
||||
or (to_s is not None and not match_to_state(to_s.state))
|
||||
or (
|
||||
not match_all
|
||||
and from_s is not None
|
||||
and to_s is not None
|
||||
and from_s.state == to_s.state
|
||||
)
|
||||
not match_from_state(old_state)
|
||||
or not match_to_state(new_state)
|
||||
or (not match_all and old_state == new_state)
|
||||
):
|
||||
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:
|
||||
call_action()
|
||||
return
|
||||
|
@ -13,20 +13,37 @@ from homeassistant.helpers.event import async_track_time_change
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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):
|
||||
"""Listen for state changes based on configuration."""
|
||||
at_time = config.get(CONF_AT)
|
||||
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
|
||||
at_times = config[CONF_AT]
|
||||
|
||||
@callback
|
||||
def time_automation_listener(now):
|
||||
"""Listen for time changes and calls action."""
|
||||
hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}})
|
||||
|
||||
return async_track_time_change(
|
||||
hass, time_automation_listener, hour=hours, minute=minutes, second=seconds
|
||||
)
|
||||
removes = [
|
||||
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
|
||||
|
@ -1,17 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Kontoen er allerede konfigurert",
|
||||
"no_devices": "Ingen enheter funnet p\u00e5 nettverket",
|
||||
"reauth_successful": "Tilgangstoken oppdatert"
|
||||
},
|
||||
"error": {
|
||||
"auth": "Ugyldig tilgangstoken",
|
||||
"unknown": "Ukjent Awair API-feil."
|
||||
},
|
||||
"step": {
|
||||
"reauth": {
|
||||
"data": {
|
||||
"access_token": "Tilgangstoken",
|
||||
"email": "Epost"
|
||||
},
|
||||
"description": "Skriv inn tilgangstokenet for Awair-utviklere p\u00e5 nytt."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"access_token": "Tilgangstoken",
|
||||
"email": "Epost "
|
||||
},
|
||||
"description": "Du m\u00e5 registrere deg for et Awair-utviklertilgangstoken p\u00e5: https://developer.getawair.com/onboard/login"
|
||||
|
26
homeassistant/components/awair/translations/pl.json
Normal file
26
homeassistant/components/awair/translations/pl.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@
|
||||
"data": {
|
||||
"host": "Vert",
|
||||
"password": "Passord",
|
||||
"port": "Port",
|
||||
"port": "",
|
||||
"username": "Brukernavn"
|
||||
},
|
||||
"title": "Sett opp Axis enhet"
|
||||
|
121
homeassistant/components/azure_devops/__init__.py
Normal file
121
homeassistant/components/azure_devops/__init__.py
Normal 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,
|
||||
}
|
134
homeassistant/components/azure_devops/config_flow.py
Normal file
134
homeassistant/components/azure_devops/config_flow.py
Normal 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,
|
||||
},
|
||||
)
|
11
homeassistant/components/azure_devops/const.py
Normal file
11
homeassistant/components/azure_devops/const.py
Normal 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"
|
8
homeassistant/components/azure_devops/manifest.json
Normal file
8
homeassistant/components/azure_devops/manifest.json
Normal 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"]
|
||||
}
|
148
homeassistant/components/azure_devops/sensor.py
Normal file
148
homeassistant/components/azure_devops/sensor.py
Normal 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
|
33
homeassistant/components/azure_devops/strings.json
Normal file
33
homeassistant/components/azure_devops/strings.json
Normal 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"
|
||||
}
|
33
homeassistant/components/azure_devops/translations/ca.json
Normal file
33
homeassistant/components/azure_devops/translations/ca.json
Normal 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"
|
||||
}
|
33
homeassistant/components/azure_devops/translations/en.json
Normal file
33
homeassistant/components/azure_devops/translations/en.json
Normal 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"
|
||||
}
|
33
homeassistant/components/azure_devops/translations/es.json
Normal file
33
homeassistant/components/azure_devops/translations/es.json
Normal 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"
|
||||
}
|
33
homeassistant/components/azure_devops/translations/it.json
Normal file
33
homeassistant/components/azure_devops/translations/it.json
Normal 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"
|
||||
}
|
33
homeassistant/components/azure_devops/translations/lb.json
Normal file
33
homeassistant/components/azure_devops/translations/lb.json
Normal 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"
|
||||
}
|
33
homeassistant/components/azure_devops/translations/no.json
Normal file
33
homeassistant/components/azure_devops/translations/no.json
Normal 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"
|
||||
}
|
33
homeassistant/components/azure_devops/translations/ru.json
Normal file
33
homeassistant/components/azure_devops/translations/ru.json
Normal 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"
|
||||
}
|
19
homeassistant/components/azure_devops/translations/uk.json
Normal file
19
homeassistant/components/azure_devops/translations/uk.json
Normal 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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -131,7 +131,7 @@
|
||||
"on": "H\u00famedo"
|
||||
},
|
||||
"motion": {
|
||||
"off": "Sin movimiento",
|
||||
"off": "No detectado",
|
||||
"on": "Detectado"
|
||||
},
|
||||
"occupancy": {
|
||||
|
12
homeassistant/components/blebox/translations/nl.json
Normal file
12
homeassistant/components/blebox/translations/nl.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "IP-adres",
|
||||
"port": "Poort"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "IP adresse",
|
||||
"port": "Port"
|
||||
"port": ""
|
||||
},
|
||||
"description": "Konfigurer BleBox-en til \u00e5 integreres med Home Assistant.",
|
||||
"title": "Konfigurere BleBox-enheten"
|
||||
|
@ -1,32 +1,25 @@
|
||||
"""Support for Blink Home Camera System."""
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
|
||||
from blinkpy.auth import Auth
|
||||
from blinkpy.blinkpy import Blink
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.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,
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.components.blink.const import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DEVICE_ID,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
SERVICE_REFRESH,
|
||||
SERVICE_SAVE_VIDEO,
|
||||
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__)
|
||||
|
||||
@ -35,58 +28,50 @@ SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
|
||||
)
|
||||
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(entry):
|
||||
def _blink_startup_wrapper(hass, entry):
|
||||
"""Startup wrapper for blink."""
|
||||
blink = Blink(
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
motion_interval=DEFAULT_OFFSET,
|
||||
legacy_subdomain=False,
|
||||
no_prompt=True,
|
||||
device_id=DEVICE_ID,
|
||||
)
|
||||
blink = Blink()
|
||||
auth_data = deepcopy(dict(entry.data))
|
||||
blink.auth = Auth(auth_data, no_prompt=True)
|
||||
blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
try:
|
||||
blink.login_response = entry.data["login_response"]
|
||||
blink.setup_params(entry.data["login_response"])
|
||||
except KeyError:
|
||||
blink.get_auth_token()
|
||||
if blink.start():
|
||||
blink.setup_post_verify()
|
||||
elif blink.auth.check_key_required():
|
||||
_LOGGER.debug("Attempting a reauth flow")
|
||||
_reauth_flow_wrapper(hass, auth_data)
|
||||
|
||||
blink.setup_params(entry.data["login_response"])
|
||||
blink.setup_post_verify()
|
||||
return blink
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up a config entry."""
|
||||
hass.data[DOMAIN] = {}
|
||||
if DOMAIN not in config:
|
||||
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
|
||||
)
|
||||
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):
|
||||
"""Set up a Blink component."""
|
||||
hass.data[DOMAIN] = {}
|
||||
return True
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -95,12 +80,11 @@ async def async_setup_entry(hass, entry):
|
||||
_async_import_options_from_data_if_missing(hass, entry)
|
||||
|
||||
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:
|
||||
_LOGGER.error("Blink unavailable for setup")
|
||||
return False
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
@ -118,7 +102,7 @@ async def async_setup_entry(hass, entry):
|
||||
def send_pin(call):
|
||||
"""Call blink to send new 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,
|
||||
)
|
||||
|
||||
|
@ -1,10 +1,16 @@
|
||||
"""Config flow to configure Blink."""
|
||||
import logging
|
||||
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.auth import Auth, LoginError, TokenRefreshFailed
|
||||
from blinkpy.blinkpy import Blink, BlinkSetupError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
from homeassistant.components.blink.const import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DEVICE_ID,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_PIN,
|
||||
@ -13,36 +19,36 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import DEFAULT_OFFSET, DEFAULT_SCAN_INTERVAL, DEVICE_ID, DOMAIN
|
||||
|
||||
_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."""
|
||||
response = await hass.async_add_executor_job(blink.get_auth_token)
|
||||
if not response:
|
||||
try:
|
||||
auth.startup()
|
||||
except (LoginError, TokenRefreshFailed):
|
||||
raise InvalidAuth
|
||||
if blink.key_required:
|
||||
if auth.check_key_required():
|
||||
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):
|
||||
"""Handle a Blink config flow."""
|
||||
|
||||
VERSION = 1
|
||||
VERSION = 2
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the blink flow."""
|
||||
self.blink = None
|
||||
self.data = {
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
"login_response": None,
|
||||
}
|
||||
self.auth = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
@ -53,28 +59,19 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initiated by the user."""
|
||||
errors = {}
|
||||
data = {CONF_USERNAME: "", CONF_PASSWORD: "", "device_id": DEVICE_ID}
|
||||
if user_input is not None:
|
||||
self.data[CONF_USERNAME] = user_input["username"]
|
||||
self.data[CONF_PASSWORD] = user_input["password"]
|
||||
data[CONF_USERNAME] = user_input["username"]
|
||||
data[CONF_PASSWORD] = user_input["password"]
|
||||
|
||||
await self.async_set_unique_id(self.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,
|
||||
)
|
||||
self.auth = Auth(data, no_prompt=True)
|
||||
await self.async_set_unique_id(data[CONF_USERNAME])
|
||||
|
||||
try:
|
||||
response = await validate_input(self.hass, self.blink)
|
||||
self.data["login_response"] = response
|
||||
return self.async_create_entry(title=DOMAIN, data=self.data,)
|
||||
await self.hass.async_add_executor_job(
|
||||
validate_input, self.hass, self.auth
|
||||
)
|
||||
return self._async_finish_flow()
|
||||
except Require2FA:
|
||||
return await self.async_step_2fa()
|
||||
except InvalidAuth:
|
||||
@ -94,23 +91,40 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_2fa(self, user_input=None):
|
||||
"""Handle 2FA step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
pin = user_input.get(CONF_PIN)
|
||||
if await self.hass.async_add_executor_job(
|
||||
self.blink.login_handler.send_auth_key, self.blink, pin
|
||||
):
|
||||
return await self.async_step_user(user_input=self.data)
|
||||
try:
|
||||
valid_token = await self.hass.async_add_executor_job(
|
||||
_send_blink_2fa_pin, self.auth, pin
|
||||
)
|
||||
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(
|
||||
step_id="2fa",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Optional("pin"): vol.All(str, vol.Length(min=1))}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data):
|
||||
"""Import blink config from configuration.yaml."""
|
||||
return await self.async_step_user(import_data)
|
||||
async def async_step_reauth(self, entry_data):
|
||||
"""Perform reauth upon migration of old entries."""
|
||||
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):
|
||||
|
@ -2,6 +2,7 @@
|
||||
DOMAIN = "blink"
|
||||
DEVICE_ID = "Home Assistant"
|
||||
|
||||
CONF_MIGRATE = "migrate"
|
||||
CONF_CAMERA = "camera"
|
||||
CONF_ALARM_CONTROL_PANEL = "alarm_control_panel"
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "blink",
|
||||
"name": "Blink",
|
||||
"documentation": "https://www.home-assistant.io/integrations/blink",
|
||||
"requirements": ["blinkpy==0.15.1"],
|
||||
"requirements": ["blinkpy==0.16.3"],
|
||||
"codeowners": ["@fronzbot"],
|
||||
"config_flow": true
|
||||
}
|
||||
|
@ -11,11 +11,13 @@
|
||||
"2fa": {
|
||||
"title": "Two-factor authentication",
|
||||
"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": {
|
||||
"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%]"
|
||||
},
|
||||
"abort": {
|
||||
|
@ -1,17 +1,24 @@
|
||||
"""The Bond integration."""
|
||||
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.const import CONF_ACCESS_TOKEN, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import SLOW_UPDATE_WARNING
|
||||
|
||||
from .const import DOMAIN
|
||||
from .utils import BondHub
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = ["cover", "fan", "light", "switch"]
|
||||
_API_TIMEOUT = SLOW_UPDATE_WARNING - 1
|
||||
|
||||
|
||||
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]
|
||||
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)
|
||||
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
|
||||
|
||||
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.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
|
@ -1,42 +1,45 @@
|
||||
"""Config flow for Bond integration."""
|
||||
from json import JSONDecodeError
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from bond import Bond
|
||||
from requests.exceptions import ConnectionError as RequestConnectionError
|
||||
from aiohttp import ClientConnectionError, ClientResponseError
|
||||
from bond_api import Bond
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
|
||||
from homeassistant import config_entries, exceptions
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
DATA_SCHEMA_USER = vol.Schema(
|
||||
{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."""
|
||||
|
||||
def authenticate(bond_hub: Bond) -> bool:
|
||||
try:
|
||||
bond_hub.getDeviceIds()
|
||||
return True
|
||||
except RequestConnectionError:
|
||||
raise CannotConnect
|
||||
except JSONDecodeError:
|
||||
return False
|
||||
try:
|
||||
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")
|
||||
|
||||
bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN])
|
||||
|
||||
if not await hass.async_add_executor_job(authenticate, bond):
|
||||
raise InvalidAuth
|
||||
|
||||
# Return info that you want to store in the config entry.
|
||||
return {"title": data[CONF_HOST]}
|
||||
# Return unique ID from the hub to be stored in the config entry.
|
||||
return version["bondid"]
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
@ -45,30 +48,73 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
_discovered: dict = None
|
||||
|
||||
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 = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
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 await self._try_create_entry(user_input)
|
||||
except InputValidationError as error:
|
||||
errors["base"] = error.base
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
async def _try_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
bond_id = await _validate_input(data)
|
||||
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):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
class InputValidationError(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot proceed due to invalid input."""
|
||||
|
||||
def __init__(self, base: str):
|
||||
"""Initialize with error base."""
|
||||
super().__init__()
|
||||
self.base = base
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""Constants for the Bond integration."""
|
||||
|
||||
DOMAIN = "bond"
|
||||
|
||||
CONF_BOND_ID: str = "bond_id"
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Support for Bond covers."""
|
||||
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.config_entries import ConfigEntry
|
||||
@ -21,12 +21,10 @@ async def async_setup_entry(
|
||||
"""Set up Bond cover devices."""
|
||||
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
devices = await hass.async_add_executor_job(hub.get_bond_devices)
|
||||
|
||||
covers = [
|
||||
BondCover(hub, device)
|
||||
for device in devices
|
||||
if device.type == DeviceTypes.MOTORIZED_SHADES
|
||||
for device in hub.devices
|
||||
if device.type == DeviceType.MOTORIZED_SHADES
|
||||
]
|
||||
|
||||
async_add_entities(covers, True)
|
||||
@ -41,30 +39,28 @@ class BondCover(BondEntity, CoverEntity):
|
||||
|
||||
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
|
||||
def device_class(self) -> Optional[str]:
|
||||
"""Get device class."""
|
||||
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
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed or not."""
|
||||
return self._closed
|
||||
|
||||
def open_cover(self, **kwargs: Any) -> None:
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""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."""
|
||||
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."""
|
||||
self._hub.bond.hold(self._device.device_id)
|
||||
await self._hub.bond.action(self._device.device_id, Action.hold())
|
||||
|
@ -1,24 +1,35 @@
|
||||
"""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 aiohttp import ClientError
|
||||
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
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."""
|
||||
|
||||
def __init__(self, hub: BondHub, device: BondDevice):
|
||||
"""Initialize entity with API and device info."""
|
||||
self._hub = hub
|
||||
self._device = device
|
||||
self._available = True
|
||||
|
||||
@property
|
||||
def unique_id(self) -> Optional[str]:
|
||||
"""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
|
||||
def name(self) -> Optional[str]:
|
||||
@ -37,4 +48,30 @@ class BondEntity:
|
||||
@property
|
||||
def assumed_state(self) -> bool:
|
||||
"""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
|
||||
|
@ -2,7 +2,7 @@
|
||||
import math
|
||||
from typing import Any, Callable, List, Optional
|
||||
|
||||
from bond import DeviceTypes, Directions
|
||||
from bond_api import Action, DeviceType, Direction
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
DIRECTION_FORWARD,
|
||||
@ -32,12 +32,8 @@ async def async_setup_entry(
|
||||
"""Set up Bond fan devices."""
|
||||
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
devices = await hass.async_add_executor_job(hub.get_bond_devices)
|
||||
|
||||
fans = [
|
||||
BondFan(hub, device)
|
||||
for device in devices
|
||||
if device.type == DeviceTypes.CEILING_FAN
|
||||
BondFan(hub, device) for device in hub.devices if DeviceType.is_fan(device.type)
|
||||
]
|
||||
|
||||
async_add_entities(fans, True)
|
||||
@ -54,6 +50,11 @@ class BondFan(BondEntity, FanEntity):
|
||||
self._speed: 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
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
@ -74,7 +75,7 @@ class BondFan(BondEntity, FanEntity):
|
||||
return None
|
||||
|
||||
# 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)
|
||||
return self.speed_list[ha_speed]
|
||||
|
||||
@ -87,21 +88,14 @@ class BondFan(BondEntity, FanEntity):
|
||||
def current_direction(self) -> Optional[str]:
|
||||
"""Return fan rotation direction."""
|
||||
direction = None
|
||||
if self._direction == Directions.FORWARD:
|
||||
if self._direction == Direction.FORWARD:
|
||||
direction = DIRECTION_FORWARD
|
||||
elif self._direction == Directions.REVERSE:
|
||||
elif self._direction == Direction.REVERSE:
|
||||
direction = DIRECTION_REVERSE
|
||||
|
||||
return direction
|
||||
|
||||
def update(self):
|
||||
"""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:
|
||||
async def async_set_speed(self, speed: str) -> None:
|
||||
"""Set the desired speed for the fan."""
|
||||
max_speed = self._device.props.get("max_speed", 3)
|
||||
if speed == SPEED_LOW:
|
||||
@ -110,21 +104,27 @@ class BondFan(BondEntity, FanEntity):
|
||||
bond_speed = max_speed
|
||||
else:
|
||||
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."""
|
||||
if speed is not None:
|
||||
self.set_speed(speed)
|
||||
self._hub.bond.turnOn(self._device.device_id)
|
||||
await self.async_set_speed(speed)
|
||||
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."""
|
||||
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."""
|
||||
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)
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Support for Bond lights."""
|
||||
from typing import Any, Callable, List, Optional
|
||||
|
||||
from bond import DeviceTypes
|
||||
from bond_api import Action, DeviceType
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
@ -26,21 +26,19 @@ async def async_setup_entry(
|
||||
"""Set up Bond light devices."""
|
||||
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
devices = await hass.async_add_executor_job(hub.get_bond_devices)
|
||||
|
||||
lights = [
|
||||
lights: List[Entity] = [
|
||||
BondLight(hub, device)
|
||||
for device in devices
|
||||
if device.type == DeviceTypes.CEILING_FAN and device.supports_light()
|
||||
for device in hub.devices
|
||||
if DeviceType.is_fan(device.type) and device.supports_light()
|
||||
]
|
||||
async_add_entities(lights, True)
|
||||
|
||||
fireplaces = [
|
||||
fireplaces: List[Entity] = [
|
||||
BondFireplace(hub, device)
|
||||
for device in devices
|
||||
if device.type == DeviceTypes.FIREPLACE
|
||||
for device in hub.devices
|
||||
if DeviceType.is_fireplace(device.type)
|
||||
]
|
||||
async_add_entities(fireplaces, True)
|
||||
|
||||
async_add_entities(lights + fireplaces, True)
|
||||
|
||||
|
||||
class BondLight(BondEntity, LightEntity):
|
||||
@ -49,26 +47,49 @@ class BondLight(BondEntity, LightEntity):
|
||||
def __init__(self, hub: BondHub, device: BondDevice):
|
||||
"""Create HA entity representing Bond fan."""
|
||||
super().__init__(hub, device)
|
||||
|
||||
self._brightness: 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
|
||||
def is_on(self) -> bool:
|
||||
"""Return if light is currently on."""
|
||||
return self._light == 1
|
||||
|
||||
def update(self):
|
||||
"""Fetch assumed state of the light from the hub using API."""
|
||||
state: dict = self._hub.bond.getDeviceState(self._device.device_id)
|
||||
self._light = state.get("light")
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
"""Return the brightness of this light between 1..255."""
|
||||
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."""
|
||||
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."""
|
||||
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):
|
||||
@ -82,6 +103,10 @@ class BondFireplace(BondEntity, LightEntity):
|
||||
# Bond flame level, 0-100
|
||||
self._flame: Optional[int] = None
|
||||
|
||||
def _apply_state(self, state: dict):
|
||||
self._power = state.get("power")
|
||||
self._flame = state.get("flame")
|
||||
|
||||
@property
|
||||
def supported_features(self) -> Optional[int]:
|
||||
"""Flag brightness as supported feature to represent flame level."""
|
||||
@ -92,18 +117,18 @@ class BondFireplace(BondEntity, LightEntity):
|
||||
"""Return True if power is on."""
|
||||
return self._power == 1
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the fireplace on."""
|
||||
self._hub.bond.turnOn(self._device.device_id)
|
||||
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
if brightness:
|
||||
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."""
|
||||
self._hub.bond.turnOff(self._device.device_id)
|
||||
await self._hub.bond.action(self._device.device_id, Action.turn_off())
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
@ -114,9 +139,3 @@ class BondFireplace(BondEntity, LightEntity):
|
||||
def icon(self) -> Optional[str]:
|
||||
"""Show fireplace icon for the entity."""
|
||||
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")
|
||||
|
@ -3,10 +3,8 @@
|
||||
"name": "Bond",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bond",
|
||||
"requirements": [
|
||||
"bond-home==0.0.9"
|
||||
],
|
||||
"codeowners": [
|
||||
"@prystupa"
|
||||
]
|
||||
"requirements": ["bond-api==0.1.8"],
|
||||
"zeroconf": ["_bond._tcp.local."],
|
||||
"codeowners": ["@prystupa"],
|
||||
"quality_scale": "platinum"
|
||||
}
|
||||
|
@ -1,6 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Bond: {bond_id} ({host})",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Do you want to set up {bond_id}?",
|
||||
"data": {
|
||||
"access_token": "[%key:common::config_flow::data::access_token%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
@ -12,6 +19,9 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
"""Support for Bond generic devices."""
|
||||
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.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from ..switch import SwitchEntity
|
||||
from .const import DOMAIN
|
||||
from .entity import BondEntity
|
||||
from .utils import BondDevice, BondHub
|
||||
@ -21,12 +21,10 @@ async def async_setup_entry(
|
||||
"""Set up Bond generic devices."""
|
||||
hub: BondHub = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
devices = await hass.async_add_executor_job(hub.get_bond_devices)
|
||||
|
||||
switches = [
|
||||
BondSwitch(hub, device)
|
||||
for device in devices
|
||||
if device.type == DeviceTypes.GENERIC_DEVICE
|
||||
for device in hub.devices
|
||||
if DeviceType.is_generic(device.type)
|
||||
]
|
||||
|
||||
async_add_entities(switches, True)
|
||||
@ -41,20 +39,18 @@ class BondSwitch(BondEntity, SwitchEntity):
|
||||
|
||||
self._power: Optional[bool] = None
|
||||
|
||||
def _apply_state(self, state: dict):
|
||||
self._power = state.get("power")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if power is on."""
|
||||
return self._power == 1
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""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."""
|
||||
self._hub.bond.turnOff(self._device.device_id)
|
||||
|
||||
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")
|
||||
await self._hub.bond.action(self._device.device_id, Action.turn_off())
|
||||
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "El dispositiu ja est\u00e0 configurat"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Ha fallat la connexi\u00f3",
|
||||
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
|
||||
|
16
homeassistant/components/bond/translations/cs.json
Normal file
16
homeassistant/components/bond/translations/cs.json
Normal 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} ?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"flow_title": "Bond: {bond_id} ({host})",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"data": {
|
||||
"access_token": "Access Token"
|
||||
},
|
||||
"description": "Do you want to set up {bond_id}?"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"access_token": "Access Token",
|
||||
|
@ -1,11 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "El dispositivo ya est\u00e1 configurado"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "No se pudo conectar",
|
||||
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
|
||||
"unknown": "Error inesperado"
|
||||
},
|
||||
"flow_title": "Bond: {bond_id} ({host})",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"data": {
|
||||
"access_token": "Token de acceso"
|
||||
},
|
||||
"description": "\u00bfQuieres configurar {bond_id}?"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"access_token": "Token de acceso",
|
||||
|
@ -1,11 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi",
|
||||
"invalid_auth": "Autenticazione non valida",
|
||||
"unknown": "Errore imprevisto"
|
||||
},
|
||||
"flow_title": "Bond: {bond_id} ({host})",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"data": {
|
||||
"access_token": "Token di accesso"
|
||||
},
|
||||
"description": "Vuoi configurare {bond_id}?"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"access_token": "Token di accesso",
|
||||
|
17
homeassistant/components/bond/translations/lb.json
Normal file
17
homeassistant/components/bond/translations/lb.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
homeassistant/components/bond/translations/no.json
Normal file
27
homeassistant/components/bond/translations/no.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
homeassistant/components/bond/translations/pl.json
Normal file
17
homeassistant/components/bond/translations/pl.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,21 @@
|
||||
{
|
||||
"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": {
|
||||
"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.",
|
||||
"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": {
|
||||
"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": {
|
||||
"data": {
|
||||
"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
Loading…
x
Reference in New Issue
Block a user