Merge pull request #38785 from home-assistant/rc

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

View File

@ -8,6 +8,10 @@ omit =
homeassistant/scripts/*.py
# 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/*

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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")
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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,8 +17,10 @@
"user": {
"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."

View File

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

View File

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

View File

@ -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):

View File

@ -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."

View File

@ -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"
}

View File

@ -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):

View File

@ -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"

View File

@ -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"

View File

@ -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,

View File

@ -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())

View File

@ -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()

View File

@ -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"]
}

View File

@ -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],

View File

@ -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."
}
}

View File

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

View File

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

View File

@ -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,6 +394,9 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
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):
@ -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,6 +476,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self._async_detach_triggers()
self._async_detach_triggers = None
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():

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"

View File

@ -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()
blink.setup_params(entry.data["login_response"])
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)
return blink
def _reauth_flow_wrapper(hass, data):
"""Reauth flow wrapper."""
hass.add_job(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": "reauth"}, data=data
)
)
persistent_notification.async_create(
hass,
"Blink configuration migrated to a new version. Please go to the integrations page to re-configure (such as sending a new 2FA key).",
"Blink Migration",
)
async def async_setup(hass, config):
"""Set up a config entry."""
"""Set up a Blink component."""
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
)
)
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,
)

View File

@ -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):

View File

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

View File

@ -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
}

View File

@ -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": {

View File

@ -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,

View File

@ -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
bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN])
version = await bond.version()
# call to non-version API is needed to validate authentication
await bond.devices()
except ClientConnectionError:
raise InputValidationError("cannot_connect")
except ClientResponseError as error:
if error.status == 401:
raise InputValidationError("invalid_auth")
raise InputValidationError("unknown")
except Exception:
_LOGGER.exception("Unexpected exception")
raise InputValidationError("unknown")
if not await hass.async_add_executor_job(authenticate, bond):
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

View File

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

View File

@ -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())

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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"
}

View File

@ -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%]"
}
}
}

View File

@ -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())

View File

@ -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",

View File

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

View File

@ -1,11 +1,21 @@
{
"config": {
"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",

View File

@ -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",

View File

@ -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",

View File

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

View File

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

View File

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

View File

@ -1,11 +1,21 @@
{
"config": {
"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