mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Merge pull request #44175 from home-assistant/rc
This commit is contained in:
commit
3600dec6e0
37
.coveragerc
37
.coveragerc
@ -48,7 +48,9 @@ omit =
|
||||
homeassistant/components/anel_pwrctrl/switch.py
|
||||
homeassistant/components/anthemav/media_player.py
|
||||
homeassistant/components/apcupsd/*
|
||||
homeassistant/components/apple_tv/*
|
||||
homeassistant/components/apple_tv/__init__.py
|
||||
homeassistant/components/apple_tv/media_player.py
|
||||
homeassistant/components/apple_tv/remote.py
|
||||
homeassistant/components/aqualogic/*
|
||||
homeassistant/components/aquostv/media_player.py
|
||||
homeassistant/components/arcam_fmj/media_player.py
|
||||
@ -65,6 +67,9 @@ omit =
|
||||
homeassistant/components/asterisk_mbox/*
|
||||
homeassistant/components/aten_pe/*
|
||||
homeassistant/components/atome/*
|
||||
homeassistant/components/aurora/__init__.py
|
||||
homeassistant/components/aurora/binary_sensor.py
|
||||
homeassistant/components/aurora/const.py
|
||||
homeassistant/components/aurora_abb_powerone/sensor.py
|
||||
homeassistant/components/avea/light.py
|
||||
homeassistant/components/avion/light.py
|
||||
@ -259,6 +264,11 @@ omit =
|
||||
homeassistant/components/fibaro/*
|
||||
homeassistant/components/filesize/sensor.py
|
||||
homeassistant/components/fints/sensor.py
|
||||
homeassistant/components/fireservicerota/__init__.py
|
||||
homeassistant/components/fireservicerota/binary_sensor.py
|
||||
homeassistant/components/fireservicerota/const.py
|
||||
homeassistant/components/fireservicerota/sensor.py
|
||||
homeassistant/components/fireservicerota/switch.py
|
||||
homeassistant/components/firmata/__init__.py
|
||||
homeassistant/components/firmata/binary_sensor.py
|
||||
homeassistant/components/firmata/board.py
|
||||
@ -533,12 +543,16 @@ omit =
|
||||
homeassistant/components/minio/*
|
||||
homeassistant/components/mitemp_bt/sensor.py
|
||||
homeassistant/components/mjpeg/camera.py
|
||||
homeassistant/components/mobile_app/*
|
||||
homeassistant/components/mochad/*
|
||||
homeassistant/components/modbus/climate.py
|
||||
homeassistant/components/modbus/cover.py
|
||||
homeassistant/components/modbus/switch.py
|
||||
homeassistant/components/modbus/sensor.py
|
||||
homeassistant/components/modem_callerid/sensor.py
|
||||
homeassistant/components/motion_blinds/__init__.py
|
||||
homeassistant/components/motion_blinds/const.py
|
||||
homeassistant/components/motion_blinds/cover.py
|
||||
homeassistant/components/motion_blinds/sensor.py
|
||||
homeassistant/components/mpchc/media_player.py
|
||||
homeassistant/components/mpd/media_player.py
|
||||
homeassistant/components/mqtt_room/sensor.py
|
||||
@ -560,7 +574,18 @@ omit =
|
||||
homeassistant/components/neato/vacuum.py
|
||||
homeassistant/components/nederlandse_spoorwegen/sensor.py
|
||||
homeassistant/components/nello/lock.py
|
||||
homeassistant/components/nest/*
|
||||
homeassistant/components/nest/__init__.py
|
||||
homeassistant/components/nest/api.py
|
||||
homeassistant/components/nest/binary_sensor.py
|
||||
homeassistant/components/nest/camera.py
|
||||
homeassistant/components/nest/camera_legacy.py
|
||||
homeassistant/components/nest/camera_sdm.py
|
||||
homeassistant/components/nest/climate.py
|
||||
homeassistant/components/nest/climate_legacy.py
|
||||
homeassistant/components/nest/climate_sdm.py
|
||||
homeassistant/components/nest/local_auth.py
|
||||
homeassistant/components/nest/sensor.py
|
||||
homeassistant/components/nest/sensor_legacy.py
|
||||
homeassistant/components/netatmo/__init__.py
|
||||
homeassistant/components/netatmo/api.py
|
||||
homeassistant/components/netatmo/camera.py
|
||||
@ -666,6 +691,7 @@ omit =
|
||||
homeassistant/components/pjlink/media_player.py
|
||||
homeassistant/components/plaato/*
|
||||
homeassistant/components/plex/media_player.py
|
||||
homeassistant/components/plex/models.py
|
||||
homeassistant/components/plex/sensor.py
|
||||
homeassistant/components/plum_lightpad/light.py
|
||||
homeassistant/components/pocketcasts/sensor.py
|
||||
@ -708,6 +734,7 @@ omit =
|
||||
homeassistant/components/rainforest_eagle/sensor.py
|
||||
homeassistant/components/raspihats/*
|
||||
homeassistant/components/raspyrfm/*
|
||||
homeassistant/components/recollect_waste/__init__.py
|
||||
homeassistant/components/recollect_waste/sensor.py
|
||||
homeassistant/components/recswitch/switch.py
|
||||
homeassistant/components/reddit/*
|
||||
@ -745,7 +772,6 @@ omit =
|
||||
homeassistant/components/russound_rnet/media_player.py
|
||||
homeassistant/components/sabnzbd/*
|
||||
homeassistant/components/saj/sensor.py
|
||||
homeassistant/components/salt/device_tracker.py
|
||||
homeassistant/components/satel_integra/*
|
||||
homeassistant/components/schluter/*
|
||||
homeassistant/components/scrape/sensor.py
|
||||
@ -821,6 +847,7 @@ omit =
|
||||
homeassistant/components/spotcrime/sensor.py
|
||||
homeassistant/components/spotify/__init__.py
|
||||
homeassistant/components/spotify/media_player.py
|
||||
homeassistant/components/spotify/system_health.py
|
||||
homeassistant/components/squeezebox/__init__.py
|
||||
homeassistant/components/squeezebox/browse_media.py
|
||||
homeassistant/components/squeezebox/media_player.py
|
||||
@ -935,7 +962,6 @@ omit =
|
||||
homeassistant/components/twilio_call/notify.py
|
||||
homeassistant/components/twilio_sms/notify.py
|
||||
homeassistant/components/twitter/notify.py
|
||||
homeassistant/components/ubee/device_tracker.py
|
||||
homeassistant/components/ubus/device_tracker.py
|
||||
homeassistant/components/ue_smart_radio/media_player.py
|
||||
homeassistant/components/unifiled/*
|
||||
@ -1054,7 +1080,6 @@ omit =
|
||||
homeassistant/components/zha/core/device.py
|
||||
homeassistant/components/zha/core/gateway.py
|
||||
homeassistant/components/zha/core/helpers.py
|
||||
homeassistant/components/zha/core/patches.py
|
||||
homeassistant/components/zha/core/registries.py
|
||||
homeassistant/components/zha/core/typing.py
|
||||
homeassistant/components/zha/entity.py
|
||||
|
@ -12,17 +12,21 @@
|
||||
"redhat.vscode-yaml",
|
||||
"esbenp.prettier-vscode"
|
||||
],
|
||||
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
|
||||
"settings": {
|
||||
"python.pythonPath": "/usr/local/bin/python",
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"python.testing.pytestArgs": ["--no-cov"],
|
||||
"python.testing.pytestEnabled": true,
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"yaml.customTags": [
|
||||
"!input scalar",
|
||||
"!secret scalar",
|
||||
"!include_dir_named scalar",
|
||||
"!include_dir_list scalar",
|
||||
|
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@ -741,7 +741,7 @@ jobs:
|
||||
-p no:sugar \
|
||||
tests
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v2.2.0
|
||||
uses: actions/upload-artifact@v2.2.1
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}-group${{ matrix.group }}
|
||||
path: .coverage
|
||||
@ -785,4 +785,4 @@ jobs:
|
||||
coverage report --fail-under=94
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1.0.14
|
||||
uses: codecov/codecov-action@v1.0.15
|
||||
|
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
# The 90 day stale policy
|
||||
# Used for: Everything (unless 30 day policy below beats it)
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@v3.0.13
|
||||
uses: actions/stale@v3.0.14
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
@ -48,7 +48,7 @@ jobs:
|
||||
# - Issues that are pending more information (incomplete issues)
|
||||
# - PRs that are not marked as new-integration
|
||||
- name: 30 days stale policy
|
||||
uses: actions/stale@v3.0.13
|
||||
uses: actions/stale@v3.0.14
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# PRs have a CLA signed label, we can misuse it to apply this policy
|
||||
|
@ -48,6 +48,7 @@ repos:
|
||||
- id: check-executables-have-shebangs
|
||||
stages: [manual]
|
||||
- id: check-json
|
||||
exclude: (.vscode|.devcontainer)
|
||||
- id: no-commit-to-branch
|
||||
args:
|
||||
- --branch=dev
|
||||
|
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Home Assistant",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "homeassistant",
|
||||
"args": ["--debug", "-c", "config"]
|
||||
}
|
||||
]
|
||||
}
|
9
.vscode/settings.default.json
vendored
Normal file
9
.vscode/settings.default.json
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
// Please keep this file in sync with settings in home-assistant/.devcontainer/devcontainer.json
|
||||
"python.formatting.provider": "black",
|
||||
// Added --no-cov to work around TypeError: message must be set
|
||||
// https://github.com/microsoft/vscode-python/issues/14067
|
||||
"python.testing.pytestArgs": ["--no-cov"],
|
||||
// https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
|
||||
"python.testing.pytestEnabled": true
|
||||
}
|
17
CODEOWNERS
17
CODEOWNERS
@ -37,6 +37,7 @@ homeassistant/components/amcrest/* @pnbruckner
|
||||
homeassistant/components/androidtv/* @JeffLIrion
|
||||
homeassistant/components/apache_kafka/* @bachya
|
||||
homeassistant/components/api/* @home-assistant/core
|
||||
homeassistant/components/apple_tv/* @postlund
|
||||
homeassistant/components/apprise/* @caronc
|
||||
homeassistant/components/aprs/* @PhilRW
|
||||
homeassistant/components/arcam_fmj/* @elupus
|
||||
@ -48,6 +49,7 @@ homeassistant/components/atag/* @MatsNL
|
||||
homeassistant/components/aten_pe/* @mtdcr
|
||||
homeassistant/components/atome/* @baqs
|
||||
homeassistant/components/august/* @bdraco
|
||||
homeassistant/components/aurora/* @djtimca
|
||||
homeassistant/components/aurora_abb_powerone/* @davet2001
|
||||
homeassistant/components/auth/* @home-assistant/core
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
@ -116,7 +118,6 @@ homeassistant/components/dunehd/* @bieniu
|
||||
homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95
|
||||
homeassistant/components/dweet/* @fabaff
|
||||
homeassistant/components/dynalite/* @ziv1234
|
||||
homeassistant/components/dyson/* @etheralm
|
||||
homeassistant/components/eafm/* @Jc2k
|
||||
homeassistant/components/ecobee/* @marthoc
|
||||
homeassistant/components/ecovacs/* @OverloadUT
|
||||
@ -131,6 +132,7 @@ homeassistant/components/emoncms/* @borpin
|
||||
homeassistant/components/emulated_kasa/* @kbickar
|
||||
homeassistant/components/enigma2/* @fbradyirl
|
||||
homeassistant/components/enocean/* @bdurrer
|
||||
homeassistant/components/enphase_envoy/* @gtdiehl
|
||||
homeassistant/components/entur_public_transport/* @hfurubotten
|
||||
homeassistant/components/environment_canada/* @michaeldavie
|
||||
homeassistant/components/ephember/* @ttroy50
|
||||
@ -144,6 +146,7 @@ homeassistant/components/ezviz/* @baqs
|
||||
homeassistant/components/fastdotcom/* @rohankapoorcom
|
||||
homeassistant/components/file/* @fabaff
|
||||
homeassistant/components/filter/* @dgomes
|
||||
homeassistant/components/fireservicerota/* @cyberjunky
|
||||
homeassistant/components/firmata/* @DaAwesomeP
|
||||
homeassistant/components/fixer/* @fabaff
|
||||
homeassistant/components/flick_electric/* @ZephireNZ
|
||||
@ -239,6 +242,7 @@ homeassistant/components/keyboard_remote/* @bendavid
|
||||
homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
|
||||
homeassistant/components/kodi/* @OnFreund @cgtobi
|
||||
homeassistant/components/konnected/* @heythisisnate @kit-klein
|
||||
homeassistant/components/kulersky/* @emlove
|
||||
homeassistant/components/lametric/* @robbiet480
|
||||
homeassistant/components/launch_library/* @ludeeus
|
||||
homeassistant/components/lcn/* @alengwenus
|
||||
@ -275,6 +279,7 @@ homeassistant/components/mobile_app/* @robbiet480
|
||||
homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
|
||||
homeassistant/components/monoprice/* @etsinko @OnFreund
|
||||
homeassistant/components/moon/* @fabaff
|
||||
homeassistant/components/motion_blinds/* @starkillerOG
|
||||
homeassistant/components/mpd/* @fabaff
|
||||
homeassistant/components/mqtt/* @home-assistant/core @emontnemery
|
||||
homeassistant/components/msteams/* @peroyvind
|
||||
@ -305,6 +310,7 @@ homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte
|
||||
homeassistant/components/nuheat/* @bdraco
|
||||
homeassistant/components/nuki/* @pschmitt @pvizeli
|
||||
homeassistant/components/numato/* @clssn
|
||||
homeassistant/components/number/* @home-assistant/core @Shulyaka
|
||||
homeassistant/components/nut/* @bdraco
|
||||
homeassistant/components/nws/* @MatthewFlamm
|
||||
homeassistant/components/nzbget/* @chriscla
|
||||
@ -335,7 +341,7 @@ homeassistant/components/pi_hole/* @fabaff @johnluetke @shenxn
|
||||
homeassistant/components/pilight/* @trekky12
|
||||
homeassistant/components/plaato/* @JohNan
|
||||
homeassistant/components/plex/* @jjlawren
|
||||
homeassistant/components/plugwise/* @CoMPaTech @bouwew
|
||||
homeassistant/components/plugwise/* @CoMPaTech @bouwew @brefra
|
||||
homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa
|
||||
homeassistant/components/point/* @fredrike
|
||||
homeassistant/components/poolsense/* @haemishkyd
|
||||
@ -360,6 +366,7 @@ homeassistant/components/raincloud/* @vanstinator
|
||||
homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert
|
||||
homeassistant/components/rainmachine/* @bachya
|
||||
homeassistant/components/random/* @fabaff
|
||||
homeassistant/components/recollect_waste/* @bachya
|
||||
homeassistant/components/rejseplanen/* @DarkFox
|
||||
homeassistant/components/repetier/* @MTrab
|
||||
homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221
|
||||
@ -374,7 +381,6 @@ homeassistant/components/rpi_power/* @shenxn @swetoast
|
||||
homeassistant/components/ruckus_unleashed/* @gabe565
|
||||
homeassistant/components/safe_mode/* @home-assistant/core
|
||||
homeassistant/components/saj/* @fredericvl
|
||||
homeassistant/components/salt/* @bjornorri
|
||||
homeassistant/components/samsungtv/* @escoand
|
||||
homeassistant/components/scene/* @home-assistant/core
|
||||
homeassistant/components/schluter/* @prairieapps
|
||||
@ -398,6 +404,7 @@ homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/sinch/* @bendikrb
|
||||
homeassistant/components/sisyphus/* @jkeljo
|
||||
homeassistant/components/sky_hub/* @rogerselwyn
|
||||
homeassistant/components/slack/* @bachya
|
||||
homeassistant/components/slide/* @ualex73
|
||||
homeassistant/components/sma/* @kellerza
|
||||
homeassistant/components/smappee/* @bsmappee
|
||||
@ -422,6 +429,7 @@ homeassistant/components/splunk/* @Bre77
|
||||
homeassistant/components/spotify/* @frenck
|
||||
homeassistant/components/sql/* @dgomes
|
||||
homeassistant/components/squeezebox/* @rajlaud
|
||||
homeassistant/components/srp_energy/* @briglx
|
||||
homeassistant/components/starline/* @anonym-tsk
|
||||
homeassistant/components/statistics/* @fabaff
|
||||
homeassistant/components/stiebel_eltron/* @fucm
|
||||
@ -469,7 +477,7 @@ homeassistant/components/transmission/* @engrbm87 @JPHutchins
|
||||
homeassistant/components/tts/* @pvizeli
|
||||
homeassistant/components/tuya/* @ollo69
|
||||
homeassistant/components/twentemilieu/* @frenck
|
||||
homeassistant/components/ubee/* @mzdrale
|
||||
homeassistant/components/twinkly/* @dr1rrb
|
||||
homeassistant/components/unifi/* @Kane610
|
||||
homeassistant/components/unifiled/* @florisvdk
|
||||
homeassistant/components/upb/* @gwww
|
||||
@ -516,7 +524,6 @@ homeassistant/components/yamaha_musiccast/* @jalmeroth
|
||||
homeassistant/components/yandex_transport/* @rishatik92 @devbis
|
||||
homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn
|
||||
homeassistant/components/yeelightsunflower/* @lindsaymarkward
|
||||
homeassistant/components/yessssms/* @flowolf
|
||||
homeassistant/components/yi/* @bachya
|
||||
homeassistant/components/zeroconf/* @bdraco
|
||||
homeassistant/components/zerproc/* @emlove
|
||||
|
@ -284,6 +284,7 @@ class AuthManager:
|
||||
self,
|
||||
user: models.User,
|
||||
name: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
group_ids: Optional[List[str]] = None,
|
||||
) -> None:
|
||||
"""Update a user."""
|
||||
@ -294,6 +295,12 @@ class AuthManager:
|
||||
kwargs["group_ids"] = group_ids
|
||||
await self._store.async_update_user(user, **kwargs)
|
||||
|
||||
if is_active is not None:
|
||||
if is_active is True:
|
||||
await self.async_activate_user(user)
|
||||
else:
|
||||
await self.async_deactivate_user(user)
|
||||
|
||||
async def async_activate_user(self, user: models.User) -> None:
|
||||
"""Activate a user."""
|
||||
await self._store.async_activate_user(user)
|
||||
|
@ -15,11 +15,7 @@ import yarl
|
||||
|
||||
from homeassistant import config as conf_util, config_entries, core, loader
|
||||
from homeassistant.components import http
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
REQUIRED_NEXT_PYTHON_DATE,
|
||||
REQUIRED_NEXT_PYTHON_VER,
|
||||
)
|
||||
from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.setup import (
|
||||
@ -142,11 +138,9 @@ async def async_setup_hass(
|
||||
_LOGGER.warning("Detected that frontend did not load. Activating safe mode")
|
||||
# Ask integrations to shut down. It's messy but we can't
|
||||
# do a clean stop without knowing what is broken
|
||||
hass.async_track_tasks()
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
|
||||
with contextlib.suppress(asyncio.TimeoutError):
|
||||
async with hass.timeout.async_timeout(10):
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_stop()
|
||||
|
||||
safe_mode = True
|
||||
old_config = hass.config
|
||||
|
@ -4,12 +4,12 @@ from copy import deepcopy
|
||||
from functools import partial
|
||||
|
||||
from abodepy import Abode
|
||||
from abodepy.exceptions import AbodeException
|
||||
from abodepy.exceptions import AbodeAuthenticationException, AbodeException
|
||||
import abodepy.helpers.timeline as TIMELINE
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DATE,
|
||||
@ -110,18 +110,34 @@ async def async_setup_entry(hass, config_entry):
|
||||
username = config_entry.data.get(CONF_USERNAME)
|
||||
password = config_entry.data.get(CONF_PASSWORD)
|
||||
polling = config_entry.data.get(CONF_POLLING)
|
||||
cache = hass.config.path(DEFAULT_CACHEDB)
|
||||
|
||||
# For previous config entries where unique_id is None
|
||||
if config_entry.unique_id is None:
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=config_entry.data[CONF_USERNAME]
|
||||
)
|
||||
|
||||
try:
|
||||
cache = hass.config.path(DEFAULT_CACHEDB)
|
||||
abode = await hass.async_add_executor_job(
|
||||
Abode, username, password, True, True, True, cache
|
||||
)
|
||||
hass.data[DOMAIN] = AbodeSystem(abode, polling)
|
||||
|
||||
except AbodeAuthenticationException as ex:
|
||||
LOGGER.error("Invalid credentials: %s", ex)
|
||||
await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_REAUTH},
|
||||
data=config_entry.data,
|
||||
)
|
||||
return False
|
||||
|
||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||
LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
||||
LOGGER.error("Unable to connect to Abode: %s", ex)
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
hass.data[DOMAIN] = AbodeSystem(abode, polling)
|
||||
|
||||
for platform in ABODE_PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, platform)
|
||||
|
@ -1,15 +1,16 @@
|
||||
"""Config flow for the Abode Security System component."""
|
||||
from abodepy import Abode
|
||||
from abodepy.exceptions import AbodeException
|
||||
from abodepy.exceptions import AbodeAuthenticationException, AbodeException
|
||||
from abodepy.helpers.errors import MFA_CODE_REQUIRED
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_BAD_REQUEST
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER # pylint: disable=unused-import
|
||||
|
||||
CONF_MFA = "mfa_code"
|
||||
CONF_POLLING = "polling"
|
||||
|
||||
|
||||
@ -25,53 +26,146 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
self.mfa_data_schema = {
|
||||
vol.Required(CONF_MFA): str,
|
||||
}
|
||||
|
||||
self._cache = None
|
||||
self._mfa_code = None
|
||||
self._password = None
|
||||
self._polling = False
|
||||
self._username = None
|
||||
|
||||
async def _async_abode_login(self, step_id):
|
||||
"""Handle login with Abode."""
|
||||
self._cache = self.hass.config.path(DEFAULT_CACHEDB)
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
Abode, self._username, self._password, True, False, False, self._cache
|
||||
)
|
||||
|
||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||
if ex.errcode == MFA_CODE_REQUIRED[0]:
|
||||
return await self.async_step_mfa()
|
||||
|
||||
LOGGER.error("Unable to connect to Abode: %s", ex)
|
||||
|
||||
if ex.errcode == HTTP_BAD_REQUEST:
|
||||
errors = {"base": "invalid_auth"}
|
||||
|
||||
else:
|
||||
errors = {"base": "cannot_connect"}
|
||||
|
||||
if errors:
|
||||
return self.async_show_form(
|
||||
step_id=step_id, data_schema=vol.Schema(self.data_schema), errors=errors
|
||||
)
|
||||
|
||||
return await self._async_create_entry()
|
||||
|
||||
async def _async_abode_mfa_login(self):
|
||||
"""Handle multi-factor authentication (MFA) login with Abode."""
|
||||
try:
|
||||
# Create instance to access login method for passing MFA code
|
||||
abode = Abode(
|
||||
auto_login=False,
|
||||
get_devices=False,
|
||||
get_automations=False,
|
||||
cache_path=self._cache,
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
abode.login, self._username, self._password, self._mfa_code
|
||||
)
|
||||
|
||||
except AbodeAuthenticationException:
|
||||
return self.async_show_form(
|
||||
step_id="mfa",
|
||||
data_schema=vol.Schema(self.mfa_data_schema),
|
||||
errors={"base": "invalid_mfa_code"},
|
||||
)
|
||||
|
||||
return await self._async_create_entry()
|
||||
|
||||
async def _async_create_entry(self):
|
||||
"""Create the config entry."""
|
||||
config_data = {
|
||||
CONF_USERNAME: self._username,
|
||||
CONF_PASSWORD: self._password,
|
||||
CONF_POLLING: self._polling,
|
||||
}
|
||||
existing_entry = await self.async_set_unique_id(self._username)
|
||||
|
||||
if existing_entry:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
existing_entry, data=config_data
|
||||
)
|
||||
# Reload the Abode config entry otherwise devices will remain unavailable
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(existing_entry.entry_id)
|
||||
)
|
||||
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_create_entry(title=self._username, data=config_data)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
if not user_input:
|
||||
return self._show_form()
|
||||
|
||||
username = user_input[CONF_USERNAME]
|
||||
password = user_input[CONF_PASSWORD]
|
||||
polling = user_input.get(CONF_POLLING, False)
|
||||
cache = self.hass.config.path(DEFAULT_CACHEDB)
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
Abode, username, password, True, True, True, cache
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=vol.Schema(self.data_schema)
|
||||
)
|
||||
|
||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||
LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
||||
if ex.errcode == HTTP_BAD_REQUEST:
|
||||
return self._show_form({"base": "invalid_auth"})
|
||||
return self._show_form({"base": "cannot_connect"})
|
||||
self._username = user_input[CONF_USERNAME]
|
||||
self._password = user_input[CONF_PASSWORD]
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME],
|
||||
data={
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password,
|
||||
CONF_POLLING: polling,
|
||||
},
|
||||
)
|
||||
return await self._async_abode_login(step_id="user")
|
||||
|
||||
@callback
|
||||
def _show_form(self, errors=None):
|
||||
"""Show the form to the user."""
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(self.data_schema),
|
||||
errors=errors if errors else {},
|
||||
)
|
||||
async def async_step_mfa(self, user_input=None):
|
||||
"""Handle a multi-factor authentication (MFA) flow."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="mfa", data_schema=vol.Schema(self.mfa_data_schema)
|
||||
)
|
||||
|
||||
self._mfa_code = user_input[CONF_MFA]
|
||||
|
||||
return await self._async_abode_mfa_login()
|
||||
|
||||
async def async_step_reauth(self, config):
|
||||
"""Handle reauthorization request from Abode."""
|
||||
self._username = config[CONF_USERNAME]
|
||||
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(self, user_input=None):
|
||||
"""Handle reauthorization flow."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME, default=self._username): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
self._username = user_input[CONF_USERNAME]
|
||||
self._password = user_input[CONF_PASSWORD]
|
||||
|
||||
return await self._async_abode_login(step_id="reauth_confirm")
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
"""Import a config entry from configuration.yaml."""
|
||||
if self._async_current_entries():
|
||||
LOGGER.warning("Only one configuration of abode is allowed.")
|
||||
LOGGER.warning("Already configured. Only a single configuration possible.")
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
self._polling = import_config.get(CONF_POLLING, False)
|
||||
|
||||
return await self.async_step_user(import_config)
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Abode",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/abode",
|
||||
"requirements": ["abodepy==1.1.0"],
|
||||
"requirements": ["abodepy==1.2.0"],
|
||||
"codeowners": ["@shred86"],
|
||||
"homekit": {
|
||||
"models": ["Abode", "Iota"]
|
||||
|
@ -7,14 +7,30 @@
|
||||
"username": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
},
|
||||
"mfa": {
|
||||
"title": "Enter your MFA code for Abode",
|
||||
"data": {
|
||||
"mfa_code": "MFA code (6-digits)"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "Fill in your Abode login information",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_mfa_code": "Invalid MFA code"
|
||||
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9",
|
||||
"single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
|
||||
"invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed"
|
||||
"invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
|
||||
"invalid_mfa_code": "Neplatn\u00fd k\u00f3d MFA"
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "K\u00f3d MFA (6 \u010d\u00edslic)"
|
||||
},
|
||||
"title": "Zadejte k\u00f3d MFA pro Abode"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Heslo",
|
||||
"username": "E-mail"
|
||||
},
|
||||
"title": "Vypl\u0148te sv\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje do Abode"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Heslo",
|
||||
|
@ -1,13 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "Re-authentication was successful",
|
||||
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication"
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"invalid_mfa_code": "Invalid MFA code"
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "MFA code (6-digits)"
|
||||
},
|
||||
"title": "Enter your MFA code for Abode"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
"username": "Email"
|
||||
},
|
||||
"title": "Fill in your Abode login information"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
|
@ -1,13 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "La reautenticaci\u00f3n fue exitosa",
|
||||
"single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "No se pudo conectar",
|
||||
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
|
||||
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
|
||||
"invalid_mfa_code": "C\u00f3digo MFA inv\u00e1lido"
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "C\u00f3digo MFA (6 d\u00edgitos)"
|
||||
},
|
||||
"title": "Introduce tu c\u00f3digo MFA para Abode"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Contrase\u00f1a",
|
||||
"username": "Correo electronico"
|
||||
},
|
||||
"title": "Rellene su informaci\u00f3n de inicio de sesi\u00f3n de Abode"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Contrase\u00f1a",
|
||||
|
@ -1,13 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "Taastuvastamine \u00f5nnestus",
|
||||
"single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00dchendamine nurjus",
|
||||
"invalid_auth": "Tuvastamise viga"
|
||||
"invalid_auth": "Tuvastamise viga",
|
||||
"invalid_mfa_code": "Kehtetu MFA-kood"
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "MFA kood (6-kohaline)"
|
||||
},
|
||||
"title": "Sisesta oma Abode MFA kood"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Salas\u00f5na",
|
||||
"username": "E-post"
|
||||
},
|
||||
"title": "Sisesta oma Abode sisselogimisteave"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Salas\u00f5na",
|
||||
|
@ -3,6 +3,9 @@
|
||||
"abort": {
|
||||
"single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -1,13 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "La riautenticazione ha avuto successo",
|
||||
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi",
|
||||
"invalid_auth": "Autenticazione non valida"
|
||||
"invalid_auth": "Autenticazione non valida",
|
||||
"invalid_mfa_code": "Codice MFA non valido"
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "Codice MFA (6 cifre)"
|
||||
},
|
||||
"title": "Inserisci il tuo codice MFA per Abode"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
"username": "E-mail"
|
||||
},
|
||||
"title": "Inserisci le tue informazioni di accesso Abode"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Password",
|
||||
|
@ -1,13 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich",
|
||||
"single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Feeler beim verbannen",
|
||||
"invalid_auth": "Ong\u00eblteg Authentifikatioun"
|
||||
"invalid_auth": "Ong\u00eblteg Authentifikatioun",
|
||||
"invalid_mfa_code": "Ong\u00ebltege MFA Code"
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "MFA code (6 Zifferen)"
|
||||
},
|
||||
"title": "G\u00ebff dain MFA code fir Abode un"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Passwuert",
|
||||
"username": "E-Mail"
|
||||
},
|
||||
"title": "F\u00ebll deng Abode Login Informatiounen aus"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Passwuert",
|
||||
|
@ -1,13 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "Reautentisering var vellykket",
|
||||
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Tilkobling mislyktes",
|
||||
"invalid_auth": "Ugyldig godkjenning"
|
||||
"invalid_auth": "Ugyldig godkjenning",
|
||||
"invalid_mfa_code": "Ugyldig MFA-kode"
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "MFA-kode (6-sifre)"
|
||||
},
|
||||
"title": "Skriv inn din MFA-kode for Abode"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Passord",
|
||||
"username": "E-post"
|
||||
},
|
||||
"title": "Fyll ut innloggingsinformasjonen for Abode"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Passord",
|
||||
|
@ -1,13 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119",
|
||||
"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_auth": "Niepoprawne uwierzytelnienie"
|
||||
"invalid_auth": "Niepoprawne uwierzytelnienie",
|
||||
"invalid_mfa_code": "Nieprawid\u0142owy kod uwierzytelniania wielosk\u0142adnikowego"
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "6-cyfrowy kod uwierzytelniania wielosk\u0142adnikowego"
|
||||
},
|
||||
"title": "Wprowad\u017a kod uwierzytelniania wielosk\u0142adnikowego dla Abode"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "Has\u0142o",
|
||||
"username": "Adres e-mail"
|
||||
},
|
||||
"title": "Wprowad\u017a informacje logowania Abode"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Has\u0142o",
|
||||
|
@ -1,13 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.",
|
||||
"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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
|
||||
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
|
||||
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
|
||||
"invalid_mfa_code": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 MFA."
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "\u041a\u043e\u0434 MFA (6 \u0446\u0438\u0444\u0440)"
|
||||
},
|
||||
"title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 MFA \u0434\u043b\u044f Abode"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
|
||||
"username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b"
|
||||
},
|
||||
"title": "Abode"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
|
||||
|
@ -1,13 +1,28 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f",
|
||||
"single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u9023\u7dda\u5931\u6557",
|
||||
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
|
||||
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
|
||||
"invalid_mfa_code": "\u591a\u6b65\u9a5f\u8a8d\u8b49\u78bc\u7121\u6548"
|
||||
},
|
||||
"step": {
|
||||
"mfa": {
|
||||
"data": {
|
||||
"mfa_code": "\u591a\u6b65\u9a5f\u8a8d\u8b49\u78bc\uff086 \u4f4d\uff09"
|
||||
},
|
||||
"title": "\u8f38\u5165 Abode \u591a\u6b65\u9a5f\u8a8d\u8b49\u78bc"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "\u5bc6\u78bc",
|
||||
"username": "\u96fb\u5b50\u90f5\u4ef6"
|
||||
},
|
||||
"title": "\u586b\u5beb Abode \u767b\u5165\u8cc7\u8a0a"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "\u5bc6\u78bc",
|
||||
|
@ -1,4 +1,20 @@
|
||||
"""Constants for AccuWeather integration."""
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
ATTR_CONDITION_EXCEPTIONAL,
|
||||
ATTR_CONDITION_FOG,
|
||||
ATTR_CONDITION_HAIL,
|
||||
ATTR_CONDITION_LIGHTNING,
|
||||
ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
ATTR_CONDITION_PARTLYCLOUDY,
|
||||
ATTR_CONDITION_POURING,
|
||||
ATTR_CONDITION_RAINY,
|
||||
ATTR_CONDITION_SNOWY,
|
||||
ATTR_CONDITION_SNOWY_RAINY,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_CONDITION_WINDY,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
@ -29,20 +45,20 @@ 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],
|
||||
ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37],
|
||||
ATTR_CONDITION_CLOUDY: [7, 8, 38],
|
||||
ATTR_CONDITION_EXCEPTIONAL: [24, 30, 31],
|
||||
ATTR_CONDITION_FOG: [11],
|
||||
ATTR_CONDITION_HAIL: [25],
|
||||
ATTR_CONDITION_LIGHTNING: [15],
|
||||
ATTR_CONDITION_LIGHTNING_RAINY: [16, 17, 41, 42],
|
||||
ATTR_CONDITION_PARTLYCLOUDY: [4, 6, 35, 36],
|
||||
ATTR_CONDITION_POURING: [18],
|
||||
ATTR_CONDITION_RAINY: [12, 13, 14, 26, 39, 40],
|
||||
ATTR_CONDITION_SNOWY: [19, 20, 21, 22, 23, 43, 44],
|
||||
ATTR_CONDITION_SNOWY_RAINY: [29],
|
||||
ATTR_CONDITION_SUNNY: [1, 2, 3, 5],
|
||||
ATTR_CONDITION_WINDY: [32],
|
||||
}
|
||||
|
||||
FORECAST_DAYS = [0, 1, 2, 3, 4]
|
||||
|
@ -31,5 +31,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "Reach AccuWeather server",
|
||||
"remaining_requests": "Remaining allowed requests"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
27
homeassistant/components/accuweather/system_health.py
Normal file
27
homeassistant/components/accuweather/system_health.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""Provide info to system health."""
|
||||
from accuweather.const import ENDPOINT
|
||||
|
||||
from homeassistant.components import system_health
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import COORDINATOR, DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def async_register(
|
||||
hass: HomeAssistant, register: system_health.SystemHealthRegistration
|
||||
) -> None:
|
||||
"""Register system health callbacks."""
|
||||
register.async_register_info(system_health_info)
|
||||
|
||||
|
||||
async def system_health_info(hass):
|
||||
"""Get info for the info page."""
|
||||
remaining_requests = list(hass.data[DOMAIN].values())[0][
|
||||
COORDINATOR
|
||||
].accuweather.requests_remaining
|
||||
|
||||
return {
|
||||
"can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT),
|
||||
"remaining_requests": remaining_requests,
|
||||
}
|
@ -31,5 +31,11 @@
|
||||
"title": "AccuWeather Options"
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "Reach AccuWeather server",
|
||||
"remaining_requests": "Remaining allowed requests"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"requests_exceeded": "Se super\u00f3 el n\u00famero permitido de solicitudes a la API de Accuweather. Tiene que esperar o cambiar la clave de API."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Si necesita ayuda con la configuraci\u00f3n, eche un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/accuweather/ \n\nAlgunos sensores no est\u00e1n habilitados de forma predeterminada. Puede habilitarlos en el registro de entidades despu\u00e9s de la configuraci\u00f3n de integraci\u00f3n. La previsi\u00f3n meteorol\u00f3gica no est\u00e1 habilitada de forma predeterminada. Puede habilitarlo en las opciones de 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 habilita el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -31,5 +31,11 @@
|
||||
"title": "AccuWeatheri valikud"
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "\u00dchendu Accuweatheri serveriga",
|
||||
"remaining_requests": "Lubatud taotlusi on j\u00e4\u00e4nud"
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
"name": "Rollease Acmeda Automate",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/acmeda",
|
||||
"requirements": ["aiopulse==0.4.0"],
|
||||
"requirements": ["aiopulse==0.4.2"],
|
||||
"codeowners": [
|
||||
"@atmurray"
|
||||
]
|
||||
|
@ -6,7 +6,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"id": "Verts-ID"
|
||||
"id": "Vert ID"
|
||||
},
|
||||
"title": "Velg en hub du vil legge til"
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -9,7 +9,7 @@
|
||||
},
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?",
|
||||
"description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?",
|
||||
"title": "AdGuard Home przez dodatek Hass.io"
|
||||
},
|
||||
"user": {
|
||||
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Conectar"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
homeassistant/components/advantage_air/translations/hu.json
Normal file
17
homeassistant/components/advantage_air/translations/hu.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "IP c\u00edm",
|
||||
"port": "Port"
|
||||
},
|
||||
"description": "Csatlakozzon az Advantage Air fali t\u00e1blag\u00e9p API-j\u00e1hoz.",
|
||||
"title": "Csatlakoz\u00e1s"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
homeassistant/components/advantage_air/translations/ka.json
Normal file
17
homeassistant/components/advantage_air/translations/ka.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8",
|
||||
"port": "\u10de\u10dd\u10e0\u10e2\u10d8"
|
||||
},
|
||||
"description": "\u10d3\u10d0\u10e3\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d3\u10d8\u10d7 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Advantage Air API-\u10e1 \u10d9\u10d4\u10d3\u10d4\u10da\u10d6\u10d4 \u10d3\u10d0\u10db\u10dd\u10dc\u10e2\u10d0\u10df\u10d4\u10d1\u10e3\u10da\u10d8 \u10e2\u10d0\u10d1\u10da\u10d4\u10e2\u10d8\u10d7",
|
||||
"title": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Pove\u017eite se"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
7
homeassistant/components/agent_dvr/translations/ka.json
Normal file
7
homeassistant/components/agent_dvr/translations/ka.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1"
|
||||
}
|
||||
}
|
||||
}
|
@ -19,5 +19,10 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "Reach Airly server"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
22
homeassistant/components/airly/system_health.py
Normal file
22
homeassistant/components/airly/system_health.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""Provide info to system health."""
|
||||
from airly import Airly
|
||||
|
||||
from homeassistant.components import system_health
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
|
||||
@callback
|
||||
def async_register(
|
||||
hass: HomeAssistant, register: system_health.SystemHealthRegistration
|
||||
) -> None:
|
||||
"""Register system health callbacks."""
|
||||
register.async_register_info(system_health_info)
|
||||
|
||||
|
||||
async def system_health_info(hass):
|
||||
"""Get info for the info page."""
|
||||
return {
|
||||
"can_reach_server": system_health.async_check_can_reach_url(
|
||||
hass, Airly.AIRLY_API_URL
|
||||
)
|
||||
}
|
@ -19,5 +19,10 @@
|
||||
"title": "Airly"
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "Reach Airly server"
|
||||
}
|
||||
}
|
||||
}
|
@ -19,5 +19,10 @@
|
||||
"title": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"can_reach_server": "\u00dchendu Airly serveriga"
|
||||
}
|
||||
}
|
||||
}
|
@ -49,7 +49,7 @@ DATA_LISTENER = "listener"
|
||||
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
|
||||
DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119")
|
||||
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
|
||||
|
||||
|
||||
@callback
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s",
|
||||
"general_error": "Ismeretlen hiba t\u00f6rt\u00e9nt."
|
||||
},
|
||||
"step": {
|
||||
@ -15,6 +16,11 @@
|
||||
"data": {
|
||||
"password": "Jelsz\u00f3"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API kulcs"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
homeassistant/components/airvisual/translations/ka.json
Normal file
15
homeassistant/components/airvisual/translations/ka.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "\u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10e3\u10da\u10d8 \u10e0\u10d4-\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "API Key"
|
||||
},
|
||||
"title": "AirVisual \u10e0\u10d4-\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,9 +15,9 @@
|
||||
"is_triggered": "{entity_name} on h\u00e4iret andnud"
|
||||
},
|
||||
"trigger_type": {
|
||||
"armed_away": "{entity_name} valvestatus",
|
||||
"armed_home": "{entity_name} valvestatus kodure\u017eiimis",
|
||||
"armed_night": "{entity_name} valvestatus \u00f6\u00f6re\u017eiimis",
|
||||
"armed_away": "{entity_name} valvestati",
|
||||
"armed_home": "{entity_name} valvestati kodure\u017eiimis",
|
||||
"armed_night": "{entity_name} valvestati \u00f6\u00f6re\u017eiimis",
|
||||
"disarmed": "{entity_name} v\u00f5eti valvest maha",
|
||||
"triggered": "{entity_name} andis h\u00e4iret"
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
{
|
||||
"config": {
|
||||
"create_entry": {
|
||||
"default": "Conectado con \u00e9xito a AlarmDecoder."
|
||||
},
|
||||
"step": {
|
||||
"protocol": {
|
||||
"data": {
|
||||
"device_baudrate": "Tasa de baudios del dispositivo",
|
||||
"device_path": "Ruta del dispositivo"
|
||||
},
|
||||
"title": "Configurar los ajustes de conexi\u00f3n"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"protocol": "Protocolo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"arm_settings": {
|
||||
"data": {
|
||||
"alt_night_mode": "Modo nocturno alternativo"
|
||||
}
|
||||
},
|
||||
"init": {
|
||||
"data": {
|
||||
"edit_select": "Editar"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
homeassistant/components/alarmdecoder/translations/hu.json
Normal file
10
homeassistant/components/alarmdecoder/translations/hu.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"config": {
|
||||
"create_entry": {
|
||||
"default": "Sikeres csatlakoz\u00e1s az AlarmDecoderhez."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Naprava je \u017ee nastavljena"
|
||||
}
|
||||
}
|
||||
}
|
@ -412,10 +412,17 @@ class AlexaLockController(AlexaCapability):
|
||||
"en-AU",
|
||||
"en-CA",
|
||||
"en-GB",
|
||||
"en-IN",
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"es-MX",
|
||||
"es-US",
|
||||
"fr-CA",
|
||||
"fr-FR",
|
||||
"hi-IN",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"pt-BR",
|
||||
}
|
||||
|
||||
def name(self):
|
||||
@ -454,6 +461,7 @@ class AlexaSceneController(AlexaCapability):
|
||||
|
||||
supported_locales = {
|
||||
"de-DE",
|
||||
"en-AU",
|
||||
"en-CA",
|
||||
"en-GB",
|
||||
"en-IN",
|
||||
@ -461,6 +469,7 @@ class AlexaSceneController(AlexaCapability):
|
||||
"es-ES",
|
||||
"fr-FR",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
}
|
||||
|
||||
def __init__(self, entity, supports_deactivation):
|
||||
@ -488,8 +497,10 @@ class AlexaBrightnessController(AlexaCapability):
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"fr-FR",
|
||||
"hi-IN",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"pt-BR",
|
||||
}
|
||||
|
||||
def name(self):
|
||||
@ -532,8 +543,10 @@ class AlexaColorController(AlexaCapability):
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"fr-FR",
|
||||
"hi-IN",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"pt-BR",
|
||||
}
|
||||
|
||||
def name(self):
|
||||
@ -581,8 +594,10 @@ class AlexaColorTemperatureController(AlexaCapability):
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"fr-FR",
|
||||
"hi-IN",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"pt-BR",
|
||||
}
|
||||
|
||||
def name(self):
|
||||
@ -669,7 +684,18 @@ class AlexaSpeaker(AlexaCapability):
|
||||
https://developer.amazon.com/docs/device-apis/alexa-speaker.html
|
||||
"""
|
||||
|
||||
supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"}
|
||||
supported_locales = {
|
||||
"de-DE",
|
||||
"en-AU",
|
||||
"en-CA",
|
||||
"en-GB",
|
||||
"en-IN",
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"es-MX",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
}
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
@ -716,7 +742,16 @@ class AlexaStepSpeaker(AlexaCapability):
|
||||
https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html
|
||||
"""
|
||||
|
||||
supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"}
|
||||
supported_locales = {
|
||||
"de-DE",
|
||||
"en-AU",
|
||||
"en-CA",
|
||||
"en-GB",
|
||||
"en-IN",
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"it-IT",
|
||||
}
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
@ -866,7 +901,16 @@ class AlexaContactSensor(AlexaCapability):
|
||||
https://developer.amazon.com/docs/device-apis/alexa-contactsensor.html
|
||||
"""
|
||||
|
||||
supported_locales = {"en-CA", "en-US", "it-IT"}
|
||||
supported_locales = {
|
||||
"de-DE",
|
||||
"en-AU",
|
||||
"en-CA",
|
||||
"en-IN",
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
}
|
||||
|
||||
def __init__(self, hass, entity):
|
||||
"""Initialize the entity."""
|
||||
@ -905,7 +949,17 @@ class AlexaMotionSensor(AlexaCapability):
|
||||
https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html
|
||||
"""
|
||||
|
||||
supported_locales = {"en-CA", "en-US", "it-IT"}
|
||||
supported_locales = {
|
||||
"de-DE",
|
||||
"en-AU",
|
||||
"en-CA",
|
||||
"en-IN",
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"pt-BR",
|
||||
}
|
||||
|
||||
def __init__(self, hass, entity):
|
||||
"""Initialize the entity."""
|
||||
@ -955,6 +1009,7 @@ class AlexaThermostatController(AlexaCapability):
|
||||
"fr-FR",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"pt-BR",
|
||||
}
|
||||
|
||||
def __init__(self, hass, entity):
|
||||
@ -1127,7 +1182,7 @@ class AlexaSecurityPanelController(AlexaCapability):
|
||||
"fr-FR",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"pt_BR",
|
||||
"pt-BR",
|
||||
}
|
||||
|
||||
def __init__(self, hass, entity):
|
||||
@ -1509,7 +1564,7 @@ class AlexaRangeController(AlexaCapability):
|
||||
min_value = float(self.entity.attributes[input_number.ATTR_MIN])
|
||||
max_value = float(self.entity.attributes[input_number.ATTR_MAX])
|
||||
precision = float(self.entity.attributes.get(input_number.ATTR_STEP, 1))
|
||||
unit = self.entity.attributes.get(input_number.ATTR_UNIT_OF_MEASUREMENT)
|
||||
unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
self._resource = AlexaPresetResource(
|
||||
["Value", AlexaGlobalCatalog.SETTING_PRESET],
|
||||
@ -1623,6 +1678,7 @@ class AlexaToggleController(AlexaCapability):
|
||||
"fr-FR",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"pt-BR",
|
||||
}
|
||||
|
||||
def __init__(self, entity, instance, non_controllable=False):
|
||||
@ -1679,7 +1735,21 @@ class AlexaChannelController(AlexaCapability):
|
||||
https://developer.amazon.com/docs/device-apis/alexa-channelcontroller.html
|
||||
"""
|
||||
|
||||
supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"}
|
||||
supported_locales = {
|
||||
"de-DE",
|
||||
"en-AU",
|
||||
"en-CA",
|
||||
"en-GB",
|
||||
"en-IN",
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"es-MX",
|
||||
"fr-FR",
|
||||
"hi-IN",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"pt-BR",
|
||||
}
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
@ -1693,7 +1763,6 @@ class AlexaDoorbellEventSource(AlexaCapability):
|
||||
"""
|
||||
|
||||
supported_locales = {
|
||||
"en-US",
|
||||
"de-DE",
|
||||
"en-AU",
|
||||
"en-CA",
|
||||
@ -1702,8 +1771,10 @@ class AlexaDoorbellEventSource(AlexaCapability):
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"es-MX",
|
||||
"es-US",
|
||||
"fr-CA",
|
||||
"fr-FR",
|
||||
"hi-IN",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
}
|
||||
@ -1723,7 +1794,7 @@ class AlexaPlaybackStateReporter(AlexaCapability):
|
||||
https://developer.amazon.com/docs/device-apis/alexa-playbackstatereporter.html
|
||||
"""
|
||||
|
||||
supported_locales = {"de-DE", "en-GB", "en-US", "fr-FR"}
|
||||
supported_locales = {"de-DE", "en-GB", "en-US", "es-MX", "fr-FR"}
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
@ -1761,7 +1832,7 @@ class AlexaSeekController(AlexaCapability):
|
||||
https://developer.amazon.com/docs/device-apis/alexa-seekcontroller.html
|
||||
"""
|
||||
|
||||
supported_locales = {"de-DE", "en-GB", "en-US"}
|
||||
supported_locales = {"de-DE", "en-GB", "en-US", "es-MX"}
|
||||
|
||||
def name(self):
|
||||
"""Return the Alexa API name of this interface."""
|
||||
@ -1833,7 +1904,7 @@ class AlexaEqualizerController(AlexaCapability):
|
||||
https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-equalizercontroller.html
|
||||
"""
|
||||
|
||||
supported_locales = {"en-US"}
|
||||
supported_locales = {"de-DE", "en-IN", "en-US", "es-ES", "it-IT", "ja-JP", "pt-BR"}
|
||||
VALID_SOUND_MODES = {
|
||||
"MOVIE",
|
||||
"MUSIC",
|
||||
@ -1929,8 +2000,10 @@ class AlexaCameraStreamController(AlexaCapability):
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"fr-FR",
|
||||
"hi-IN",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"pt-BR",
|
||||
}
|
||||
|
||||
def name(self):
|
||||
|
@ -995,14 +995,14 @@ async def async_api_set_mode(hass, config, directive, context):
|
||||
|
||||
# Fan Direction
|
||||
if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
|
||||
_, direction = mode.split(".")
|
||||
direction = mode.split(".")[1]
|
||||
if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD):
|
||||
service = fan.SERVICE_SET_DIRECTION
|
||||
data[fan.ATTR_DIRECTION] = direction
|
||||
|
||||
# Cover Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
_, position = mode.split(".")
|
||||
position = mode.split(".")[1]
|
||||
|
||||
if position == cover.STATE_CLOSED:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"create_entry": {
|
||||
"default": "Sikeres autentik\u00e1ci\u00f3"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0",
|
||||
"missing_configuration": "\u10d4\u10e1 \u10d9\u10dd\u10db\u10de\u10dd\u10dc\u10d4\u10dc\u10e2\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8. \u10d2\u10d7\u10ee\u10dd\u10d5\u10d7 \u10db\u10d8\u10e7\u10d5\u10d4\u10d7 \u10d3\u10dd\u10d9\u10e3\u10db\u10d4\u10dc\u10e2\u10d0\u10ea\u10d8\u10d0\u10e1"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
"""Support for Ambient Weather Station Service."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aioambient import Client
|
||||
from aioambient.errors import WebsocketError
|
||||
@ -39,12 +38,11 @@ from .const import (
|
||||
CONF_APP_KEY,
|
||||
DATA_CLIENT,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
TYPE_BINARY_SENSOR,
|
||||
TYPE_SENSOR,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_CONFIG = "config"
|
||||
|
||||
DEFAULT_SOCKET_MIN_RETRY = 15
|
||||
@ -307,7 +305,7 @@ async def async_setup_entry(hass, config_entry):
|
||||
hass.loop.create_task(ambient.ws_connect())
|
||||
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient
|
||||
except WebsocketError as err:
|
||||
_LOGGER.error("Config entry failed: %s", err)
|
||||
LOGGER.error("Config entry failed: %s", err)
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
async def _async_disconnect_websocket(*_):
|
||||
@ -337,7 +335,7 @@ async def async_migrate_entry(hass, config_entry):
|
||||
"""Migrate old entry."""
|
||||
version = config_entry.version
|
||||
|
||||
_LOGGER.debug("Migrating from version %s", version)
|
||||
LOGGER.debug("Migrating from version %s", version)
|
||||
|
||||
# 1 -> 2: Unique ID format changed, so delete and re-import:
|
||||
if version == 1:
|
||||
@ -350,7 +348,7 @@ async def async_migrate_entry(hass, config_entry):
|
||||
version = config_entry.version = 2
|
||||
hass.config_entries.async_update_entry(config_entry)
|
||||
|
||||
_LOGGER.info("Migration to version %s successful", version)
|
||||
LOGGER.info("Migration to version %s successful", version)
|
||||
|
||||
return True
|
||||
|
||||
@ -377,7 +375,7 @@ class AmbientStation:
|
||||
try:
|
||||
await connect()
|
||||
except WebsocketError as err:
|
||||
_LOGGER.error("Error with the websocket connection: %s", err)
|
||||
LOGGER.error("Error with the websocket connection: %s", err)
|
||||
self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480)
|
||||
async_call_later(self._hass, self._ws_reconnect_delay, connect)
|
||||
|
||||
@ -386,13 +384,13 @@ class AmbientStation:
|
||||
|
||||
def on_connect():
|
||||
"""Define a handler to fire when the websocket is connected."""
|
||||
_LOGGER.info("Connected to websocket")
|
||||
LOGGER.info("Connected to websocket")
|
||||
|
||||
def on_data(data):
|
||||
"""Define a handler to fire when the data is received."""
|
||||
mac_address = data["macAddress"]
|
||||
if data != self.stations[mac_address][ATTR_LAST_DATA]:
|
||||
_LOGGER.debug("New data received: %s", data)
|
||||
LOGGER.debug("New data received: %s", data)
|
||||
self.stations[mac_address][ATTR_LAST_DATA] = data
|
||||
async_dispatcher_send(
|
||||
self._hass, f"ambient_station_data_update_{mac_address}"
|
||||
@ -400,7 +398,7 @@ class AmbientStation:
|
||||
|
||||
def on_disconnect():
|
||||
"""Define a handler to fire when the websocket is disconnected."""
|
||||
_LOGGER.info("Disconnected from websocket")
|
||||
LOGGER.info("Disconnected from websocket")
|
||||
|
||||
def on_subscribed(data):
|
||||
"""Define a handler to fire when the subscription is set."""
|
||||
@ -408,7 +406,7 @@ class AmbientStation:
|
||||
if station["macAddress"] in self.stations:
|
||||
continue
|
||||
|
||||
_LOGGER.debug("New station subscription: %s", data)
|
||||
LOGGER.debug("New station subscription: %s", data)
|
||||
|
||||
# Only create entities based on the data coming through the socket.
|
||||
# If the user is monitoring brightness (in W/m^2), make sure we also
|
||||
|
@ -1,5 +1,8 @@
|
||||
"""Define constants for the Ambient PWS component."""
|
||||
import logging
|
||||
|
||||
DOMAIN = "ambient_station"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
ATTR_LAST_DATA = "last_data"
|
||||
ATTR_MONITORED_CONDITIONS = "monitored_conditions"
|
||||
|
@ -236,7 +236,7 @@ class AmcrestCam(Camera):
|
||||
# streaming via ffmpeg
|
||||
|
||||
streaming_url = self._rtsp_url
|
||||
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
stream = CameraMjpeg(self._ffmpeg.binary)
|
||||
await stream.open_camera(streaming_url, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
try:
|
||||
|
@ -4,7 +4,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
||||
"requirements": [
|
||||
"adb-shell[async]==0.2.1",
|
||||
"androidtv[async]==0.0.54",
|
||||
"androidtv[async]==0.0.56",
|
||||
"pure-python-adb[async]==0.3.0.dev0"
|
||||
],
|
||||
"codeowners": ["@JeffLIrion"]
|
||||
|
@ -56,7 +56,7 @@ STREAM_PING_PAYLOAD = "ping"
|
||||
STREAM_PING_INTERVAL = 50 # seconds
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Register the API with the HTTP interface."""
|
||||
hass.http.register_view(APIStatusView)
|
||||
hass.http.register_view(APIEventStream)
|
||||
|
@ -1,273 +1,363 @@
|
||||
"""Support for Apple TV."""
|
||||
"""The Apple TV integration."""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Sequence, TypeVar, Union
|
||||
from random import randrange
|
||||
|
||||
from pyatv import AppleTVDevice, connect_to_apple_tv, scan_for_apple_tvs
|
||||
from pyatv.exceptions import DeviceAuthenticationError
|
||||
import voluptuous as vol
|
||||
from pyatv import connect, exceptions, scan
|
||||
from pyatv.const import Protocol
|
||||
|
||||
from homeassistant.components.discovery import SERVICE_APPLE_TV
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||
from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_NAME,
|
||||
CONF_PROTOCOL,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "apple_tv"
|
||||
|
||||
SERVICE_SCAN = "apple_tv_scan"
|
||||
SERVICE_AUTHENTICATE = "apple_tv_authenticate"
|
||||
|
||||
ATTR_ATV = "atv"
|
||||
ATTR_POWER = "power"
|
||||
|
||||
CONF_LOGIN_ID = "login_id"
|
||||
CONF_START_OFF = "start_off"
|
||||
CONF_CREDENTIALS = "credentials"
|
||||
|
||||
DEFAULT_NAME = "Apple TV"
|
||||
|
||||
DATA_APPLE_TV = "data_apple_tv"
|
||||
DATA_ENTITIES = "data_apple_tv_entities"
|
||||
BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes
|
||||
|
||||
KEY_CONFIG = "apple_tv_configuring"
|
||||
NOTIFICATION_TITLE = "Apple TV Notification"
|
||||
NOTIFICATION_ID = "apple_tv_notification"
|
||||
|
||||
NOTIFICATION_AUTH_ID = "apple_tv_auth_notification"
|
||||
NOTIFICATION_AUTH_TITLE = "Apple TV Authentication"
|
||||
NOTIFICATION_SCAN_ID = "apple_tv_scan_notification"
|
||||
NOTIFICATION_SCAN_TITLE = "Apple TV Scan"
|
||||
SOURCE_REAUTH = "reauth"
|
||||
|
||||
T = TypeVar("T")
|
||||
SIGNAL_CONNECTED = "apple_tv_connected"
|
||||
SIGNAL_DISCONNECTED = "apple_tv_disconnected"
|
||||
|
||||
|
||||
# This version of ensure_list interprets an empty dict as no value
|
||||
def ensure_list(value: Union[T, Sequence[T]]) -> Sequence[T]:
|
||||
"""Wrap value in list if it is not one."""
|
||||
if value is None or (isinstance(value, dict) and not value):
|
||||
return []
|
||||
return value if isinstance(value, list) else [value]
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
ensure_list,
|
||||
[
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_LOGIN_ID): cv.string,
|
||||
vol.Optional(CONF_CREDENTIALS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_START_OFF, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
# Currently no attributes but it might change later
|
||||
APPLE_TV_SCAN_SCHEMA = vol.Schema({})
|
||||
|
||||
APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
|
||||
def request_configuration(hass, config, atv, credentials):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
async def configuration_callback(callback_data):
|
||||
"""Handle the submitted configuration."""
|
||||
|
||||
pin = callback_data.get("pin")
|
||||
|
||||
try:
|
||||
await atv.airplay.finish_authentication(pin)
|
||||
hass.components.persistent_notification.async_create(
|
||||
f"Authentication succeeded!<br /><br />"
|
||||
f"Add the following to credentials: "
|
||||
f"in your apple_tv configuration:<br /><br />{credentials}",
|
||||
title=NOTIFICATION_AUTH_TITLE,
|
||||
notification_id=NOTIFICATION_AUTH_ID,
|
||||
)
|
||||
except DeviceAuthenticationError as ex:
|
||||
hass.components.persistent_notification.async_create(
|
||||
f"Authentication failed! Did you enter correct PIN?<br /><br />Details: {ex}",
|
||||
title=NOTIFICATION_AUTH_TITLE,
|
||||
notification_id=NOTIFICATION_AUTH_ID,
|
||||
)
|
||||
|
||||
hass.async_add_job(configurator.request_done, instance)
|
||||
|
||||
instance = configurator.request_config(
|
||||
"Apple TV Authentication",
|
||||
configuration_callback,
|
||||
description="Please enter PIN code shown on screen.",
|
||||
submit_caption="Confirm",
|
||||
fields=[{"id": "pin", "name": "PIN Code", "type": "password"}],
|
||||
)
|
||||
|
||||
|
||||
async def scan_apple_tvs(hass):
|
||||
"""Scan for devices and present a notification of the ones found."""
|
||||
|
||||
atvs = await scan_for_apple_tvs(hass.loop, timeout=3)
|
||||
|
||||
devices = []
|
||||
for atv in atvs:
|
||||
login_id = atv.login_id
|
||||
if login_id is None:
|
||||
login_id = "Home Sharing disabled"
|
||||
devices.append(
|
||||
f"Name: {atv.name}<br />Host: {atv.address}<br />Login ID: {login_id}"
|
||||
)
|
||||
|
||||
if not devices:
|
||||
devices = ["No device(s) found"]
|
||||
|
||||
found_devices = "<br /><br />".join(devices)
|
||||
|
||||
hass.components.persistent_notification.async_create(
|
||||
f"The following devices were found:<br /><br />{found_devices}",
|
||||
title=NOTIFICATION_SCAN_TITLE,
|
||||
notification_id=NOTIFICATION_SCAN_ID,
|
||||
)
|
||||
PLATFORMS = [MP_DOMAIN, REMOTE_DOMAIN]
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the Apple TV component."""
|
||||
if DATA_APPLE_TV not in hass.data:
|
||||
hass.data[DATA_APPLE_TV] = {}
|
||||
"""Set up the Apple TV integration."""
|
||||
return True
|
||||
|
||||
async def async_service_handler(service):
|
||||
"""Handle service calls."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if service.service == SERVICE_SCAN:
|
||||
hass.async_add_job(scan_apple_tvs, hass)
|
||||
return
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up a config entry for Apple TV."""
|
||||
manager = AppleTVManager(hass, entry)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.unique_id] = manager
|
||||
|
||||
if entity_ids:
|
||||
devices = [
|
||||
device
|
||||
for device in hass.data[DATA_ENTITIES]
|
||||
if device.entity_id in entity_ids
|
||||
async def on_hass_stop(event):
|
||||
"""Stop push updates when hass stops."""
|
||||
await manager.disconnect()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
|
||||
|
||||
async def setup_platforms():
|
||||
"""Set up platforms and initiate connection."""
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
else:
|
||||
devices = hass.data[DATA_ENTITIES]
|
||||
|
||||
for device in devices:
|
||||
if service.service != SERVICE_AUTHENTICATE:
|
||||
continue
|
||||
|
||||
atv = device.atv
|
||||
credentials = await atv.airplay.generate_credentials()
|
||||
await atv.airplay.load_credentials(credentials)
|
||||
_LOGGER.debug("Generated new credentials: %s", credentials)
|
||||
await atv.airplay.start_authentication()
|
||||
hass.async_add_job(request_configuration, hass, config, atv, credentials)
|
||||
|
||||
async def atv_discovered(service, info):
|
||||
"""Set up an Apple TV that was auto discovered."""
|
||||
await _setup_atv(
|
||||
hass,
|
||||
config,
|
||||
{
|
||||
CONF_NAME: info["name"],
|
||||
CONF_HOST: info["host"],
|
||||
CONF_LOGIN_ID: info["properties"]["hG"],
|
||||
CONF_START_OFF: False,
|
||||
},
|
||||
)
|
||||
await manager.init()
|
||||
|
||||
discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
|
||||
|
||||
tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])]
|
||||
if tasks:
|
||||
await asyncio.wait(tasks)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SCAN, async_service_handler, schema=APPLE_TV_SCAN_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_AUTHENTICATE,
|
||||
async_service_handler,
|
||||
schema=APPLE_TV_AUTHENTICATE_SCHEMA,
|
||||
)
|
||||
hass.async_create_task(setup_platforms())
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _setup_atv(hass, hass_config, atv_config):
|
||||
"""Set up an Apple TV."""
|
||||
|
||||
name = atv_config.get(CONF_NAME)
|
||||
host = atv_config.get(CONF_HOST)
|
||||
login_id = atv_config.get(CONF_LOGIN_ID)
|
||||
start_off = atv_config.get(CONF_START_OFF)
|
||||
credentials = atv_config.get(CONF_CREDENTIALS)
|
||||
|
||||
if host in hass.data[DATA_APPLE_TV]:
|
||||
return
|
||||
|
||||
details = AppleTVDevice(name, host, login_id)
|
||||
session = async_get_clientsession(hass)
|
||||
atv = connect_to_apple_tv(details, hass.loop, session=session)
|
||||
if credentials:
|
||||
await atv.airplay.load_credentials(credentials)
|
||||
|
||||
power = AppleTVPowerManager(hass, atv, start_off)
|
||||
hass.data[DATA_APPLE_TV][host] = {ATTR_ATV: atv, ATTR_POWER: power}
|
||||
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hass, "media_player", DOMAIN, atv_config, hass_config
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload an Apple TV config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, platform)
|
||||
for platform in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
manager = hass.data[DOMAIN].pop(entry.unique_id)
|
||||
await manager.disconnect()
|
||||
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(hass, "remote", DOMAIN, atv_config, hass_config)
|
||||
)
|
||||
return unload_ok
|
||||
|
||||
|
||||
class AppleTVPowerManager:
|
||||
"""Manager for global power management of an Apple TV.
|
||||
class AppleTVEntity(Entity):
|
||||
"""Device that sends commands to an Apple TV."""
|
||||
|
||||
An instance is used per device to share the same power state between
|
||||
several platforms.
|
||||
"""
|
||||
def __init__(self, name, identifier, manager):
|
||||
"""Initialize device."""
|
||||
self.atv = None
|
||||
self.manager = manager
|
||||
self._name = name
|
||||
self._identifier = identifier
|
||||
|
||||
def __init__(self, hass, atv, is_off):
|
||||
"""Initialize power manager."""
|
||||
self.hass = hass
|
||||
self.atv = atv
|
||||
self.listeners = []
|
||||
self._is_on = not is_off
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle when an entity is about to be added to Home Assistant."""
|
||||
|
||||
def init(self):
|
||||
"""Initialize power management."""
|
||||
if self._is_on:
|
||||
self.atv.push_updater.start()
|
||||
@callback
|
||||
def _async_connected(atv):
|
||||
"""Handle that a connection was made to a device."""
|
||||
self.atv = atv
|
||||
self.async_device_connected(atv)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_disconnected():
|
||||
"""Handle that a connection to a device was lost."""
|
||||
self.async_device_disconnected()
|
||||
self.atv = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, f"{SIGNAL_CONNECTED}_{self._identifier}", _async_connected
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{SIGNAL_DISCONNECTED}_{self._identifier}",
|
||||
_async_disconnected,
|
||||
)
|
||||
)
|
||||
|
||||
def async_device_connected(self, atv):
|
||||
"""Handle when connection is made to device."""
|
||||
|
||||
def async_device_disconnected(self):
|
||||
"""Handle when connection was lost to device."""
|
||||
|
||||
@property
|
||||
def turned_on(self):
|
||||
"""Return true if device is on or off."""
|
||||
return self._is_on
|
||||
def device_info(self):
|
||||
"""Return the device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._identifier)},
|
||||
"manufacturer": "Apple",
|
||||
"name": self.name,
|
||||
}
|
||||
|
||||
def set_power_on(self, value):
|
||||
"""Change if a device is on or off."""
|
||||
if value != self._is_on:
|
||||
self._is_on = value
|
||||
if not self._is_on:
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._identifier
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for Apple TV."""
|
||||
return False
|
||||
|
||||
|
||||
class AppleTVManager:
|
||||
"""Connection and power manager for an Apple TV.
|
||||
|
||||
An instance is used per device to share the same power state between
|
||||
several platforms. It also manages scanning and connection establishment
|
||||
in case of problems.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, config_entry):
|
||||
"""Initialize power manager."""
|
||||
self.config_entry = config_entry
|
||||
self.hass = hass
|
||||
self.atv = None
|
||||
self._is_on = not config_entry.options.get(CONF_START_OFF, False)
|
||||
self._connection_attempts = 0
|
||||
self._connection_was_lost = False
|
||||
self._task = None
|
||||
|
||||
async def init(self):
|
||||
"""Initialize power management."""
|
||||
if self._is_on:
|
||||
await self.connect()
|
||||
|
||||
def connection_lost(self, _):
|
||||
"""Device was unexpectedly disconnected.
|
||||
|
||||
This is a callback function from pyatv.interface.DeviceListener.
|
||||
"""
|
||||
_LOGGER.warning('Connection lost to Apple TV "%s"', self.atv.name)
|
||||
if self.atv:
|
||||
self.atv.close()
|
||||
self.atv = None
|
||||
self._connection_was_lost = True
|
||||
self._dispatch_send(SIGNAL_DISCONNECTED)
|
||||
self._start_connect_loop()
|
||||
|
||||
def connection_closed(self):
|
||||
"""Device connection was (intentionally) closed.
|
||||
|
||||
This is a callback function from pyatv.interface.DeviceListener.
|
||||
"""
|
||||
if self.atv:
|
||||
self.atv.close()
|
||||
self.atv = None
|
||||
self._dispatch_send(SIGNAL_DISCONNECTED)
|
||||
self._start_connect_loop()
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to device."""
|
||||
self._is_on = True
|
||||
self._start_connect_loop()
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from device."""
|
||||
_LOGGER.debug("Disconnecting from device")
|
||||
self._is_on = False
|
||||
try:
|
||||
if self.atv:
|
||||
self.atv.push_updater.listener = None
|
||||
self.atv.push_updater.stop()
|
||||
else:
|
||||
self.atv.push_updater.start()
|
||||
self.atv.close()
|
||||
self.atv = None
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
self._task = None
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("An error occurred while disconnecting")
|
||||
|
||||
for listener in self.listeners:
|
||||
self.hass.async_create_task(listener.async_update_ha_state())
|
||||
def _start_connect_loop(self):
|
||||
"""Start background connect loop to device."""
|
||||
if not self._task and self.atv is None and self._is_on:
|
||||
self._task = asyncio.create_task(self._connect_loop())
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Not starting connect loop (%s, %s)", self.atv is None, self._is_on
|
||||
)
|
||||
|
||||
async def _connect_loop(self):
|
||||
"""Connect loop background task function."""
|
||||
_LOGGER.debug("Starting connect loop")
|
||||
|
||||
# Try to find device and connect as long as the user has said that
|
||||
# we are allowed to connect and we are not already connected.
|
||||
while self._is_on and self.atv is None:
|
||||
try:
|
||||
conf = await self._scan()
|
||||
if conf:
|
||||
await self._connect(conf)
|
||||
except exceptions.AuthenticationError:
|
||||
self._auth_problem()
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Failed to connect")
|
||||
self.atv = None
|
||||
|
||||
if self.atv is None:
|
||||
self._connection_attempts += 1
|
||||
backoff = min(
|
||||
randrange(2 ** self._connection_attempts), BACKOFF_TIME_UPPER_LIMIT
|
||||
)
|
||||
|
||||
_LOGGER.debug("Reconnecting in %d seconds", backoff)
|
||||
await asyncio.sleep(backoff)
|
||||
|
||||
_LOGGER.debug("Connect loop ended")
|
||||
self._task = None
|
||||
|
||||
def _auth_problem(self):
|
||||
"""Problem to authenticate occurred that needs intervention."""
|
||||
_LOGGER.debug("Authentication error, reconfigure integration")
|
||||
|
||||
name = self.config_entry.data.get(CONF_NAME)
|
||||
identifier = self.config_entry.unique_id
|
||||
|
||||
self.hass.components.persistent_notification.create(
|
||||
"An irrecoverable connection problem occurred when connecting to "
|
||||
f"`f{name}`. Please go to the Integrations page and reconfigure it",
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID,
|
||||
)
|
||||
|
||||
# Add to event queue as this function is called from a task being
|
||||
# cancelled from disconnect
|
||||
asyncio.create_task(self.disconnect())
|
||||
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_REAUTH},
|
||||
data={CONF_NAME: name, CONF_IDENTIFIER: identifier},
|
||||
)
|
||||
)
|
||||
|
||||
async def _scan(self):
|
||||
"""Try to find device by scanning for it."""
|
||||
identifier = self.config_entry.unique_id
|
||||
address = self.config_entry.data[CONF_ADDRESS]
|
||||
protocol = Protocol(self.config_entry.data[CONF_PROTOCOL])
|
||||
|
||||
_LOGGER.debug("Discovering device %s", identifier)
|
||||
atvs = await scan(
|
||||
self.hass.loop, identifier=identifier, protocol=protocol, hosts=[address]
|
||||
)
|
||||
if atvs:
|
||||
return atvs[0]
|
||||
|
||||
_LOGGER.debug(
|
||||
"Failed to find device %s with address %s, trying to scan",
|
||||
identifier,
|
||||
address,
|
||||
)
|
||||
|
||||
atvs = await scan(self.hass.loop, identifier=identifier, protocol=protocol)
|
||||
if atvs:
|
||||
return atvs[0]
|
||||
|
||||
_LOGGER.debug("Failed to find device %s, trying later", identifier)
|
||||
|
||||
return None
|
||||
|
||||
async def _connect(self, conf):
|
||||
"""Connect to device."""
|
||||
credentials = self.config_entry.data[CONF_CREDENTIALS]
|
||||
session = async_get_clientsession(self.hass)
|
||||
|
||||
for protocol, creds in credentials.items():
|
||||
conf.set_credentials(Protocol(int(protocol)), creds)
|
||||
|
||||
_LOGGER.debug("Connecting to device %s", self.config_entry.data[CONF_NAME])
|
||||
self.atv = await connect(conf, self.hass.loop, session=session)
|
||||
self.atv.listener = self
|
||||
|
||||
self._dispatch_send(SIGNAL_CONNECTED, self.atv)
|
||||
self._address_updated(str(conf.address))
|
||||
|
||||
self._connection_attempts = 0
|
||||
if self._connection_was_lost:
|
||||
_LOGGER.info(
|
||||
'Connection was re-established to Apple TV "%s"', self.atv.service.name
|
||||
)
|
||||
self._connection_was_lost = False
|
||||
|
||||
@property
|
||||
def is_connecting(self):
|
||||
"""Return true if connection is in progress."""
|
||||
return self._task is not None
|
||||
|
||||
def _address_updated(self, address):
|
||||
"""Update cached address in config entry."""
|
||||
_LOGGER.debug("Changing address to %s", address)
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry, data={**self.config_entry.data, CONF_ADDRESS: address}
|
||||
)
|
||||
|
||||
def _dispatch_send(self, signal, *args):
|
||||
"""Dispatch a signal to all entities managed by this manager."""
|
||||
async_dispatcher_send(
|
||||
self.hass, f"{signal}_{self.config_entry.unique_id}", *args
|
||||
)
|
||||
|
408
homeassistant/components/apple_tv/config_flow.py
Normal file
408
homeassistant/components/apple_tv/config_flow.py
Normal file
@ -0,0 +1,408 @@
|
||||
"""Config flow for Apple TV integration."""
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
from random import randrange
|
||||
|
||||
from pyatv import exceptions, pair, scan
|
||||
from pyatv.const import Protocol
|
||||
from pyatv.convert import protocol_str
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_NAME,
|
||||
CONF_PIN,
|
||||
CONF_PROTOCOL,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF
|
||||
from .const import DOMAIN # pylint: disable=unused-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEVICE_INPUT = "device_input"
|
||||
|
||||
INPUT_PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN, default=None): int})
|
||||
|
||||
DEFAULT_START_OFF = False
|
||||
PROTOCOL_PRIORITY = [Protocol.MRP, Protocol.DMAP, Protocol.AirPlay]
|
||||
|
||||
|
||||
async def device_scan(identifier, loop, cache=None):
|
||||
"""Scan for a specific device using identifier as filter."""
|
||||
|
||||
def _filter_device(dev):
|
||||
if identifier is None:
|
||||
return True
|
||||
if identifier == str(dev.address):
|
||||
return True
|
||||
if identifier == dev.name:
|
||||
return True
|
||||
return any([service.identifier == identifier for service in dev.services])
|
||||
|
||||
def _host_filter():
|
||||
try:
|
||||
return [ip_address(identifier)]
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if cache:
|
||||
matches = [atv for atv in cache if _filter_device(atv)]
|
||||
if matches:
|
||||
return cache, matches[0]
|
||||
|
||||
for hosts in [_host_filter(), None]:
|
||||
scan_result = await scan(loop, timeout=3, hosts=hosts)
|
||||
matches = [atv for atv in scan_result if _filter_device(atv)]
|
||||
|
||||
if matches:
|
||||
return scan_result, matches[0]
|
||||
|
||||
return scan_result, None
|
||||
|
||||
|
||||
def is_valid_credentials(credentials):
|
||||
"""Verify that credentials are valid for establishing a connection."""
|
||||
return (
|
||||
credentials.get(Protocol.MRP.value) is not None
|
||||
or credentials.get(Protocol.DMAP.value) is not None
|
||||
)
|
||||
|
||||
|
||||
class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Apple TV."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Get options flow for this handler."""
|
||||
return AppleTVOptionsFlow(config_entry)
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a new AppleTVConfigFlow."""
|
||||
self.target_device = None
|
||||
self.scan_result = None
|
||||
self.atv = None
|
||||
self.protocol = None
|
||||
self.pairing = None
|
||||
self.credentials = {} # Protocol -> credentials
|
||||
|
||||
async def async_step_reauth(self, info):
|
||||
"""Handle initial step when updating invalid credentials."""
|
||||
await self.async_set_unique_id(info[CONF_IDENTIFIER])
|
||||
self.target_device = info[CONF_IDENTIFIER]
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context["title_placeholders"] = {"name": info[CONF_NAME]}
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context["identifier"] = self.unique_id
|
||||
return await self.async_step_reconfigure()
|
||||
|
||||
async def async_step_reconfigure(self, user_input=None):
|
||||
"""Inform user that reconfiguration is about to start."""
|
||||
if user_input is not None:
|
||||
return await self.async_find_device_wrapper(
|
||||
self.async_begin_pairing, allow_exist=True
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="reconfigure")
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
# Be helpful to the user and look for devices
|
||||
if self.scan_result is None:
|
||||
self.scan_result, _ = await device_scan(None, self.hass.loop)
|
||||
|
||||
errors = {}
|
||||
default_suggestion = self._prefill_identifier()
|
||||
if user_input is not None:
|
||||
self.target_device = user_input[DEVICE_INPUT]
|
||||
try:
|
||||
await self.async_find_device()
|
||||
except DeviceNotFound:
|
||||
errors["base"] = "no_devices_found"
|
||||
except DeviceAlreadyConfigured:
|
||||
errors["base"] = "already_configured"
|
||||
except exceptions.NoServiceError:
|
||||
errors["base"] = "no_usable_service"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(
|
||||
self.atv.identifier, raise_on_progress=False
|
||||
)
|
||||
return await self.async_step_confirm()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(DEVICE_INPUT, default=default_suggestion): str}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={"devices": self._devices_str()},
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Handle device found via zeroconf."""
|
||||
service_type = discovery_info[CONF_TYPE]
|
||||
properties = discovery_info["properties"]
|
||||
|
||||
if service_type == "_mediaremotetv._tcp.local.":
|
||||
identifier = properties["UniqueIdentifier"]
|
||||
name = properties["Name"]
|
||||
elif service_type == "_touch-able._tcp.local.":
|
||||
identifier = discovery_info["name"].split(".")[0]
|
||||
name = properties["CtlN"]
|
||||
else:
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
await self.async_set_unique_id(identifier)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context["identifier"] = self.unique_id
|
||||
self.context["title_placeholders"] = {"name": name}
|
||||
self.target_device = identifier
|
||||
return await self.async_find_device_wrapper(self.async_step_confirm)
|
||||
|
||||
async def async_find_device_wrapper(self, next_func, allow_exist=False):
|
||||
"""Find a specific device and call another function when done.
|
||||
|
||||
This function will do error handling and bail out when an error
|
||||
occurs.
|
||||
"""
|
||||
try:
|
||||
await self.async_find_device(allow_exist)
|
||||
except DeviceNotFound:
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
except DeviceAlreadyConfigured:
|
||||
return self.async_abort(reason="already_configured")
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
return await next_func()
|
||||
|
||||
async def async_find_device(self, allow_exist=False):
|
||||
"""Scan for the selected device to discover services."""
|
||||
self.scan_result, self.atv = await device_scan(
|
||||
self.target_device, self.hass.loop, cache=self.scan_result
|
||||
)
|
||||
if not self.atv:
|
||||
raise DeviceNotFound()
|
||||
|
||||
self.protocol = self.atv.main_service().protocol
|
||||
|
||||
if not allow_exist:
|
||||
for identifier in self.atv.all_identifiers:
|
||||
if identifier in self._async_current_ids():
|
||||
raise DeviceAlreadyConfigured()
|
||||
|
||||
# If credentials were found, save them
|
||||
for service in self.atv.services:
|
||||
if service.credentials:
|
||||
self.credentials[service.protocol.value] = service.credentials
|
||||
|
||||
async def async_step_confirm(self, user_input=None):
|
||||
"""Handle user-confirmation of discovered node."""
|
||||
if user_input is not None:
|
||||
return await self.async_begin_pairing()
|
||||
return self.async_show_form(
|
||||
step_id="confirm", description_placeholders={"name": self.atv.name}
|
||||
)
|
||||
|
||||
async def async_begin_pairing(self):
|
||||
"""Start pairing process for the next available protocol."""
|
||||
self.protocol = self._next_protocol_to_pair()
|
||||
|
||||
# Dispose previous pairing sessions
|
||||
if self.pairing is not None:
|
||||
await self.pairing.close()
|
||||
self.pairing = None
|
||||
|
||||
# Any more protocols to pair? Else bail out here
|
||||
if not self.protocol:
|
||||
await self.async_set_unique_id(self.atv.main_service().identifier)
|
||||
return self._async_get_entry(
|
||||
self.atv.main_service().protocol,
|
||||
self.atv.name,
|
||||
self.credentials,
|
||||
self.atv.address,
|
||||
)
|
||||
|
||||
# Initiate the pairing process
|
||||
abort_reason = None
|
||||
session = async_get_clientsession(self.hass)
|
||||
self.pairing = await pair(
|
||||
self.atv, self.protocol, self.hass.loop, session=session
|
||||
)
|
||||
try:
|
||||
await self.pairing.begin()
|
||||
except exceptions.ConnectionFailedError:
|
||||
return await self.async_step_service_problem()
|
||||
except exceptions.BackOffError:
|
||||
abort_reason = "backoff"
|
||||
except exceptions.PairingError:
|
||||
_LOGGER.exception("Authentication problem")
|
||||
abort_reason = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
abort_reason = "unknown"
|
||||
|
||||
if abort_reason:
|
||||
if self.pairing:
|
||||
await self.pairing.close()
|
||||
return self.async_abort(reason=abort_reason)
|
||||
|
||||
# Choose step depending on if PIN is required from user or not
|
||||
if self.pairing.device_provides_pin:
|
||||
return await self.async_step_pair_with_pin()
|
||||
|
||||
return await self.async_step_pair_no_pin()
|
||||
|
||||
async def async_step_pair_with_pin(self, user_input=None):
|
||||
"""Handle pairing step where a PIN is required from the user."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
self.pairing.pin(user_input[CONF_PIN])
|
||||
await self.pairing.finish()
|
||||
self.credentials[self.protocol.value] = self.pairing.service.credentials
|
||||
return await self.async_begin_pairing()
|
||||
except exceptions.PairingError:
|
||||
_LOGGER.exception("Authentication problem")
|
||||
errors["base"] = "invalid_auth"
|
||||
except AbortFlow:
|
||||
raise
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="pair_with_pin",
|
||||
data_schema=INPUT_PIN_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={"protocol": protocol_str(self.protocol)},
|
||||
)
|
||||
|
||||
async def async_step_pair_no_pin(self, user_input=None):
|
||||
"""Handle step where user has to enter a PIN on the device."""
|
||||
if user_input is not None:
|
||||
await self.pairing.finish()
|
||||
if self.pairing.has_paired:
|
||||
self.credentials[self.protocol.value] = self.pairing.service.credentials
|
||||
return await self.async_begin_pairing()
|
||||
|
||||
await self.pairing.close()
|
||||
return self.async_abort(reason="device_did_not_pair")
|
||||
|
||||
pin = randrange(1000, stop=10000)
|
||||
self.pairing.pin(pin)
|
||||
return self.async_show_form(
|
||||
step_id="pair_no_pin",
|
||||
description_placeholders={
|
||||
"protocol": protocol_str(self.protocol),
|
||||
"pin": pin,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_service_problem(self, user_input=None):
|
||||
"""Inform user that a service will not be added."""
|
||||
if user_input is not None:
|
||||
self.credentials[self.protocol.value] = None
|
||||
return await self.async_begin_pairing()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="service_problem",
|
||||
description_placeholders={"protocol": protocol_str(self.protocol)},
|
||||
)
|
||||
|
||||
def _async_get_entry(self, protocol, name, credentials, address):
|
||||
if not is_valid_credentials(credentials):
|
||||
return self.async_abort(reason="invalid_config")
|
||||
|
||||
data = {
|
||||
CONF_PROTOCOL: protocol.value,
|
||||
CONF_NAME: name,
|
||||
CONF_CREDENTIALS: credentials,
|
||||
CONF_ADDRESS: str(address),
|
||||
}
|
||||
|
||||
self._abort_if_unique_id_configured(reload_on_update=False, updates=data)
|
||||
|
||||
return self.async_create_entry(title=name, data=data)
|
||||
|
||||
def _next_protocol_to_pair(self):
|
||||
def _needs_pairing(protocol):
|
||||
if self.atv.get_service(protocol) is None:
|
||||
return False
|
||||
return protocol.value not in self.credentials
|
||||
|
||||
for protocol in PROTOCOL_PRIORITY:
|
||||
if _needs_pairing(protocol):
|
||||
return protocol
|
||||
return None
|
||||
|
||||
def _devices_str(self):
|
||||
return ", ".join(
|
||||
[
|
||||
f"`{atv.name} ({atv.address})`"
|
||||
for atv in self.scan_result
|
||||
if atv.identifier not in self._async_current_ids()
|
||||
]
|
||||
)
|
||||
|
||||
def _prefill_identifier(self):
|
||||
# Return identifier (address) of one device that has not been paired with
|
||||
for atv in self.scan_result:
|
||||
if atv.identifier not in self._async_current_ids():
|
||||
return str(atv.address)
|
||||
return ""
|
||||
|
||||
|
||||
class AppleTVOptionsFlow(config_entries.OptionsFlow):
|
||||
"""Handle Apple TV options."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
"""Initialize Apple TV options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.options = dict(config_entry.options)
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the Apple TV options."""
|
||||
if user_input is not None:
|
||||
self.options[CONF_START_OFF] = user_input[CONF_START_OFF]
|
||||
return self.async_create_entry(title="", data=self.options)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_START_OFF,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_START_OFF, DEFAULT_START_OFF
|
||||
),
|
||||
): bool,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class DeviceNotFound(HomeAssistantError):
|
||||
"""Error to indicate device could not be found."""
|
||||
|
||||
|
||||
class DeviceAlreadyConfigured(HomeAssistantError):
|
||||
"""Error to indicate device is already configured."""
|
11
homeassistant/components/apple_tv/const.py
Normal file
11
homeassistant/components/apple_tv/const.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""Constants for the Apple TV integration."""
|
||||
|
||||
DOMAIN = "apple_tv"
|
||||
|
||||
CONF_IDENTIFIER = "identifier"
|
||||
CONF_CREDENTIALS = "credentials"
|
||||
CONF_CREDENTIALS_MRP = "mrp"
|
||||
CONF_CREDENTIALS_DMAP = "dmap"
|
||||
CONF_CREDENTIALS_AIRPLAY = "airplay"
|
||||
|
||||
CONF_START_OFF = "start_off"
|
@ -1,9 +1,17 @@
|
||||
{
|
||||
"domain": "apple_tv",
|
||||
"name": "Apple TV",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||
"requirements": ["pyatv==0.3.13"],
|
||||
"dependencies": ["configurator"],
|
||||
"requirements": [
|
||||
"pyatv==0.7.5"
|
||||
],
|
||||
"zeroconf": [
|
||||
"_mediaremotetv._tcp.local.",
|
||||
"_touch-able._tcp.local."
|
||||
],
|
||||
"after_dependencies": ["discovery"],
|
||||
"codeowners": []
|
||||
"codeowners": [
|
||||
"@postlund"
|
||||
]
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Support for Apple TV media player."""
|
||||
import logging
|
||||
|
||||
import pyatv.const as atv_const
|
||||
from pyatv.const import DeviceState, MediaType
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerEntity
|
||||
from homeassistant.components.media_player.const import (
|
||||
@ -19,9 +19,7 @@ from homeassistant.components.media_player.const import (
|
||||
SUPPORT_TURN_ON,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
STATE_PAUSED,
|
||||
@ -31,10 +29,13 @@ from homeassistant.const import (
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import ATTR_ATV, ATTR_POWER, DATA_APPLE_TV, DATA_ENTITIES
|
||||
from . import AppleTVEntity
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
SUPPORT_APPLE_TV = (
|
||||
SUPPORT_TURN_ON
|
||||
| SUPPORT_TURN_OFF
|
||||
@ -48,108 +49,61 @@ SUPPORT_APPLE_TV = (
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the Apple TV platform."""
|
||||
if not discovery_info:
|
||||
return
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Load Apple TV media player based on a config entry."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
manager = hass.data[DOMAIN][config_entry.unique_id]
|
||||
async_add_entities([AppleTvMediaPlayer(name, config_entry.unique_id, manager)])
|
||||
|
||||
# Manage entity cache for service handler
|
||||
if DATA_ENTITIES not in hass.data:
|
||||
hass.data[DATA_ENTITIES] = []
|
||||
|
||||
name = discovery_info[CONF_NAME]
|
||||
host = discovery_info[CONF_HOST]
|
||||
atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV]
|
||||
power = hass.data[DATA_APPLE_TV][host][ATTR_POWER]
|
||||
entity = AppleTvDevice(atv, name, power)
|
||||
class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
|
||||
"""Representation of an Apple TV media player."""
|
||||
|
||||
def __init__(self, name, identifier, manager, **kwargs):
|
||||
"""Initialize the Apple TV media player."""
|
||||
super().__init__(name, identifier, manager, **kwargs)
|
||||
self._playing = None
|
||||
|
||||
@callback
|
||||
def on_hass_stop(event):
|
||||
"""Stop push updates when hass stops."""
|
||||
atv.push_updater.stop()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
|
||||
|
||||
if entity not in hass.data[DATA_ENTITIES]:
|
||||
hass.data[DATA_ENTITIES].append(entity)
|
||||
|
||||
async_add_entities([entity])
|
||||
|
||||
|
||||
class AppleTvDevice(MediaPlayerEntity):
|
||||
"""Representation of an Apple TV device."""
|
||||
|
||||
def __init__(self, atv, name, power):
|
||||
"""Initialize the Apple TV device."""
|
||||
self.atv = atv
|
||||
self._name = name
|
||||
self._playing = None
|
||||
self._power = power
|
||||
self._power.listeners.append(self)
|
||||
def async_device_connected(self, atv):
|
||||
"""Handle when connection is made to device."""
|
||||
self.atv.push_updater.listener = self
|
||||
self.atv.push_updater.start()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle when an entity is about to be added to Home Assistant."""
|
||||
self._power.init()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self.atv.metadata.device_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
@callback
|
||||
def async_device_disconnected(self):
|
||||
"""Handle when connection was lost to device."""
|
||||
self.atv.push_updater.stop()
|
||||
self.atv.push_updater.listener = None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if not self._power.turned_on:
|
||||
if self.manager.is_connecting:
|
||||
return None
|
||||
if self.atv is None:
|
||||
return STATE_OFF
|
||||
|
||||
if self._playing:
|
||||
|
||||
state = self._playing.play_state
|
||||
if state in (
|
||||
atv_const.PLAY_STATE_IDLE,
|
||||
atv_const.PLAY_STATE_NO_MEDIA,
|
||||
atv_const.PLAY_STATE_LOADING,
|
||||
):
|
||||
state = self._playing.device_state
|
||||
if state in (DeviceState.Idle, DeviceState.Loading):
|
||||
return STATE_IDLE
|
||||
if state == atv_const.PLAY_STATE_PLAYING:
|
||||
if state == DeviceState.Playing:
|
||||
return STATE_PLAYING
|
||||
if state in (
|
||||
atv_const.PLAY_STATE_PAUSED,
|
||||
atv_const.PLAY_STATE_FAST_FORWARD,
|
||||
atv_const.PLAY_STATE_FAST_BACKWARD,
|
||||
atv_const.PLAY_STATE_STOPPED,
|
||||
):
|
||||
# Catch fast forward/backward here so "play" is default action
|
||||
if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
|
||||
return STATE_PAUSED
|
||||
return STATE_STANDBY # Bad or unknown state?
|
||||
return None
|
||||
|
||||
@callback
|
||||
def playstatus_update(self, updater, playing):
|
||||
def playstatus_update(self, _, playing):
|
||||
"""Print what is currently playing when it changes."""
|
||||
self._playing = playing
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def playstatus_error(self, updater, exception):
|
||||
def playstatus_error(self, _, exception):
|
||||
"""Inform about an error and restart push updates."""
|
||||
_LOGGER.warning("A %s error occurred: %s", exception.__class__, exception)
|
||||
|
||||
# This will wait 10 seconds before restarting push updates. If the
|
||||
# connection continues to fail, it will flood the log (every 10
|
||||
# seconds) until it succeeds. A better approach should probably be
|
||||
# implemented here later.
|
||||
updater.start(initial_delay=10)
|
||||
self._playing = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
@ -157,50 +111,53 @@ class AppleTvDevice(MediaPlayerEntity):
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
if self._playing:
|
||||
|
||||
media_type = self._playing.media_type
|
||||
if media_type == atv_const.MEDIA_TYPE_VIDEO:
|
||||
return MEDIA_TYPE_VIDEO
|
||||
if media_type == atv_const.MEDIA_TYPE_MUSIC:
|
||||
return MEDIA_TYPE_MUSIC
|
||||
if media_type == atv_const.MEDIA_TYPE_TV:
|
||||
return MEDIA_TYPE_TVSHOW
|
||||
return {
|
||||
MediaType.Video: MEDIA_TYPE_VIDEO,
|
||||
MediaType.Music: MEDIA_TYPE_MUSIC,
|
||||
MediaType.TV: MEDIA_TYPE_TVSHOW,
|
||||
}.get(self._playing.media_type)
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
if self._playing:
|
||||
return self._playing.total_time
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_position(self):
|
||||
"""Position of current playing media in seconds."""
|
||||
if self._playing:
|
||||
return self._playing.position
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_position_updated_at(self):
|
||||
"""Last valid time of media position."""
|
||||
state = self.state
|
||||
if state in (STATE_PLAYING, STATE_PAUSED):
|
||||
if self.state in (STATE_PLAYING, STATE_PAUSED):
|
||||
return dt_util.utcnow()
|
||||
return None
|
||||
|
||||
async def async_play_media(self, media_type, media_id, **kwargs):
|
||||
"""Send the play_media command to the media player."""
|
||||
await self.atv.airplay.play_url(media_id)
|
||||
await self.atv.stream.play_url(media_id)
|
||||
|
||||
@property
|
||||
def media_image_hash(self):
|
||||
"""Hash value for media image."""
|
||||
state = self.state
|
||||
if self._playing and state not in [STATE_OFF, STATE_IDLE]:
|
||||
return self._playing.hash
|
||||
if self._playing and state not in [None, STATE_OFF, STATE_IDLE]:
|
||||
return self.atv.metadata.artwork_id
|
||||
return None
|
||||
|
||||
async def async_get_media_image(self):
|
||||
"""Fetch media image of current playing image."""
|
||||
state = self.state
|
||||
if self._playing and state not in [STATE_OFF, STATE_IDLE]:
|
||||
return (await self.atv.metadata.artwork()), "image/png"
|
||||
artwork = await self.atv.metadata.artwork()
|
||||
if artwork:
|
||||
return artwork.bytes, artwork.mimetype
|
||||
|
||||
return None, None
|
||||
|
||||
@ -208,12 +165,8 @@ class AppleTvDevice(MediaPlayerEntity):
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
if self._playing:
|
||||
if self.state == STATE_IDLE:
|
||||
return "Nothing playing"
|
||||
title = self._playing.title
|
||||
return title if title else "No title"
|
||||
|
||||
return f"Establishing a connection to {self._name}..."
|
||||
return self._playing.title
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
@ -222,22 +175,22 @@ class AppleTvDevice(MediaPlayerEntity):
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
self._power.set_power_on(True)
|
||||
await self.manager.connect()
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn the media player off."""
|
||||
self._playing = None
|
||||
self._power.set_power_on(False)
|
||||
await self.manager.disconnect()
|
||||
|
||||
async def async_media_play_pause(self):
|
||||
"""Pause media on media player."""
|
||||
if not self._playing:
|
||||
return
|
||||
state = self.state
|
||||
if state == STATE_PAUSED:
|
||||
await self.atv.remote_control.play()
|
||||
elif state == STATE_PLAYING:
|
||||
await self.atv.remote_control.pause()
|
||||
if self._playing:
|
||||
state = self.state
|
||||
if state == STATE_PAUSED:
|
||||
await self.atv.remote_control.play()
|
||||
elif state == STATE_PLAYING:
|
||||
await self.atv.remote_control.pause()
|
||||
return None
|
||||
|
||||
async def async_media_play(self):
|
||||
"""Play media."""
|
||||
|
@ -1,46 +1,32 @@
|
||||
"""Remote control support for Apple TV."""
|
||||
from homeassistant.components import remote
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
|
||||
from . import ATTR_ATV, ATTR_POWER, DATA_APPLE_TV
|
||||
import logging
|
||||
|
||||
from homeassistant.components.remote import RemoteEntity
|
||||
from homeassistant.const import CONF_NAME
|
||||
|
||||
from . import AppleTVEntity
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the Apple TV remote platform."""
|
||||
if not discovery_info:
|
||||
return
|
||||
|
||||
name = discovery_info[CONF_NAME]
|
||||
host = discovery_info[CONF_HOST]
|
||||
atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV]
|
||||
power = hass.data[DATA_APPLE_TV][host][ATTR_POWER]
|
||||
async_add_entities([AppleTVRemote(atv, power, name)])
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Load Apple TV remote based on a config entry."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
manager = hass.data[DOMAIN][config_entry.unique_id]
|
||||
async_add_entities([AppleTVRemote(name, config_entry.unique_id, manager)])
|
||||
|
||||
|
||||
class AppleTVRemote(remote.RemoteEntity):
|
||||
class AppleTVRemote(AppleTVEntity, RemoteEntity):
|
||||
"""Device that sends commands to an Apple TV."""
|
||||
|
||||
def __init__(self, atv, power, name):
|
||||
"""Initialize device."""
|
||||
self._atv = atv
|
||||
self._name = name
|
||||
self._power = power
|
||||
self._power.listeners.append(self)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._atv.metadata.device_id
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._power.turned_on
|
||||
return self.atv is not None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -48,23 +34,21 @@ class AppleTVRemote(remote.RemoteEntity):
|
||||
return False
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
self._power.set_power_on(True)
|
||||
"""Turn the device on."""
|
||||
await self.manager.connect()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the device off.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
self._power.set_power_on(False)
|
||||
"""Turn the device off."""
|
||||
await self.manager.disconnect()
|
||||
|
||||
async def async_send_command(self, command, **kwargs):
|
||||
"""Send a command to one device."""
|
||||
if not self.is_on:
|
||||
_LOGGER.error("Unable to send commands, not connected to %s", self._name)
|
||||
return
|
||||
|
||||
for single_command in command:
|
||||
if not hasattr(self._atv.remote_control, single_command):
|
||||
if not hasattr(self.atv.remote_control, single_command):
|
||||
continue
|
||||
|
||||
await getattr(self._atv.remote_control, single_command)()
|
||||
await getattr(self.atv.remote_control, single_command)()
|
||||
|
@ -1,8 +0,0 @@
|
||||
apple_tv_authenticate:
|
||||
description: Start AirPlay device authentication.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to authenticate with.
|
||||
example: media_player.apple_tv
|
||||
apple_tv_scan:
|
||||
description: Scan for Apple TV devices.
|
64
homeassistant/components/apple_tv/strings.json
Normal file
64
homeassistant/components/apple_tv/strings.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"title": "Apple TV",
|
||||
"config": {
|
||||
"flow_title": "Apple TV: {name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Setup a new Apple TV",
|
||||
"description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add. If any devices were automatically found on your network, they are shown below.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.\n\n{devices}",
|
||||
"data": {
|
||||
"device_input": "Device"
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Device reconfiguration",
|
||||
"description": "This Apple TV is experiencing some connection difficulties and must be reconfigured."
|
||||
},
|
||||
"pair_with_pin": {
|
||||
"title": "Pairing",
|
||||
"description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.",
|
||||
"data": {
|
||||
"pin": "[%key:common::config_flow::data::pin%]"
|
||||
}
|
||||
},
|
||||
"pair_no_pin": {
|
||||
"title": "Pairing",
|
||||
"description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your Apple TV to continue."
|
||||
},
|
||||
"service_problem": {
|
||||
"title": "Failed to add service",
|
||||
"description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored."
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Confirm adding Apple TV",
|
||||
"description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"abort": {
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"device_did_not_pair": "No attempt to finish pairing process was made from the device.",
|
||||
"backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.",
|
||||
"invalid_config": "The configuration for this device is incomplete. Please try adding it again.",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Configure general device settings",
|
||||
"data": {
|
||||
"start_off": "Do not turn device on when starting Home Assistant"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
64
homeassistant/components/apple_tv/translations/en.json
Normal file
64
homeassistant/components/apple_tv/translations/en.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured_device": "Device is already configured",
|
||||
"already_in_progress": "Configuration flow is already in progress",
|
||||
"backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.",
|
||||
"device_did_not_pair": "No attempt to finish pairing process was made from the device.",
|
||||
"invalid_config": "The configuration for this device is incomplete. Please try adding it again.",
|
||||
"no_devices_found": "No devices found on the network",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Device is already configured",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"no_devices_found": "No devices found on the network",
|
||||
"no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"flow_title": "Apple TV: {name}",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!",
|
||||
"title": "Confirm adding Apple TV"
|
||||
},
|
||||
"pair_no_pin": {
|
||||
"description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your Apple TV to continue.",
|
||||
"title": "Pairing"
|
||||
},
|
||||
"pair_with_pin": {
|
||||
"data": {
|
||||
"pin": "PIN Code"
|
||||
},
|
||||
"description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.",
|
||||
"title": "Pairing"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "This Apple TV is experiencing some connection difficulties and must be reconfigured.",
|
||||
"title": "Device reconfiguration"
|
||||
},
|
||||
"service_problem": {
|
||||
"description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored.",
|
||||
"title": "Failed to add service"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"device_input": "Device"
|
||||
},
|
||||
"description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add. If any devices were automatically found on your network, they are shown below.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.\n\n{devices}",
|
||||
"title": "Setup a new Apple TV"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"start_off": "Do not turn device on when starting Home Assistant"
|
||||
},
|
||||
"description": "Configure general device settings"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Apple TV"
|
||||
}
|
64
homeassistant/components/apple_tv/translations/et.json
Normal file
64
homeassistant/components/apple_tv/translations/et.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured_device": "Seade on juba h\u00e4\u00e4lestatud",
|
||||
"already_in_progress": "Seadistamine on juba k\u00e4imas",
|
||||
"backoff": "Seade ei aktsepteeri praegu sidumisn\u00f5udeid (v\u00f5ib-olla oled liiga palju kordi vale PIN-koodi sisestanud), proovi hiljem uuesti.",
|
||||
"device_did_not_pair": "Seade ei \u00fcritatud sidumisprotsessi l\u00f5pule viia.",
|
||||
"invalid_config": "Selle seadme s\u00e4tted on puudulikud. Proovi see uuesti lisada.",
|
||||
"no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet",
|
||||
"unknown": "Ootamatu t\u00f5rge"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Seade on juba h\u00e4\u00e4lestatud",
|
||||
"invalid_auth": "Vigane autentimine",
|
||||
"no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet",
|
||||
"no_usable_service": "Leiti seade kuid ei suudetud tuvastada moodust \u00fchenduse loomiseks. Kui n\u00e4ed seda teadet pidevalt, proovi m\u00e4\u00e4rata seadme IP-aadress v\u00f5i taask\u00e4ivita Apple TV.",
|
||||
"unknown": "Ootamatu t\u00f5rge"
|
||||
},
|
||||
"flow_title": "Apple TV: {name}",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Oled Home Assistantile lisamas Apple TV-d nimega {name}.\n\n**Protsessi l\u00f5puleviimiseks pead v\u00f5ib-olla sisestama mitu PIN-koodi.**\n\nPane t\u00e4hele, et selle sidumisega * ei saa * v\u00e4lja l\u00fclitada oma Apple TV-d. Ainult Home Assistant-i meediam\u00e4ngija l\u00fclitub v\u00e4lja!",
|
||||
"title": "Kinnita Apple TV lisamine"
|
||||
},
|
||||
"pair_no_pin": {
|
||||
"description": "Teenuse {protocol} sidumine on vajalik. J\u00e4tkamiseks sisesta oma Apple TV-s PIN-kood {pin} .",
|
||||
"title": "Sidumine"
|
||||
},
|
||||
"pair_with_pin": {
|
||||
"data": {
|
||||
"pin": "PIN kood"
|
||||
},
|
||||
"description": "Vajalik on protokolli {protocol} sidumine. Sisesta ekraanil kuvatav PIN-kood. Alguse nullid j\u00e4etakse v\u00e4lja, st. sisesta 123, kui kuvatav kood on 0123.",
|
||||
"title": "Sidumine"
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Sellel Apple TV-l on \u00fchendusprobleemid ja see tuleb uuesti seadistada.",
|
||||
"title": "Seadme \u00fcmberseadistamine"
|
||||
},
|
||||
"service_problem": {
|
||||
"description": "Protokolli {protocol} sidumisel ilmnes probleem. Seda ignoreeritakse.",
|
||||
"title": "Teenuse lisamine eba\u00f5nnestus."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"device_input": "Seade"
|
||||
},
|
||||
"description": "Alustuseks sisesta lisatava Apple TV seadme nimi (nt K\u00f6\u00f6k v\u00f5i Magamistuba) v\u00f5i IP-aadress. Kui m\u00f5ni seade leiti teie v\u00f5rgust automaatselt kuvatakse see allpool. \n\n Kui ei n\u00e4e oma seadet v\u00f5i on probleeme, proovi m\u00e4\u00e4rata seadme IP-aadress. \n\n {devices}",
|
||||
"title": "Seadista uus Apple TV sidumine"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"start_off": "\u00c4ra l\u00fclita seadet Home Assistanti k\u00e4ivitamisel sisse"
|
||||
},
|
||||
"description": "Seadme \u00fclds\u00e4tete seadistamine"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": ""
|
||||
}
|
@ -23,7 +23,7 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.115")
|
||||
CONFIG_SCHEMA = cv.deprecated(DOMAIN)
|
||||
|
||||
|
||||
async def _await_cancel(task):
|
||||
|
7
homeassistant/components/arcam_fmj/translations/hu.json
Normal file
7
homeassistant/components/arcam_fmj/translations/hu.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
|
||||
}
|
||||
}
|
||||
}
|
@ -5,10 +5,6 @@
|
||||
"already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
|
||||
"cannot_connect": "Impossibile connettersi"
|
||||
},
|
||||
"error": {
|
||||
"one": "uno",
|
||||
"other": "altri"
|
||||
},
|
||||
"flow_title": "Arcam FMJ su {host}",
|
||||
"step": {
|
||||
"confirm": {
|
||||
|
@ -14,7 +14,7 @@
|
||||
"flow_title": "Arcam FMJ na {host}",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Czy chcesz doda\u0107 Arcam FMJ na \"{host}\" do Home Assistant?"
|
||||
"description": "Czy chcesz doda\u0107 Arcam FMJ na \"{host}\" do Home Assistanta?"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@ -88,7 +88,7 @@ class ArloCam(Camera):
|
||||
_LOGGER.error(error_msg)
|
||||
return
|
||||
|
||||
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
stream = CameraMjpeg(self._ffmpeg.binary)
|
||||
await stream.open_camera(video.video_url, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
try:
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "arlo",
|
||||
"name": "Arlo",
|
||||
"documentation": "https://www.home-assistant.io/integrations/arlo",
|
||||
"requirements": ["pyarlo==0.2.3"],
|
||||
"requirements": ["pyarlo==0.2.4"],
|
||||
"dependencies": ["ffmpeg"],
|
||||
"codeowners": []
|
||||
}
|
||||
|
@ -2,6 +2,6 @@
|
||||
"domain": "asuswrt",
|
||||
"name": "ASUSWRT",
|
||||
"documentation": "https://www.home-assistant.io/integrations/asuswrt",
|
||||
"requirements": ["aioasuswrt==1.3.0"],
|
||||
"requirements": ["aioasuswrt==1.3.1"],
|
||||
"codeowners": ["@kennedyshead"]
|
||||
}
|
||||
|
@ -1,184 +1,168 @@
|
||||
"""Asuswrt status sensors."""
|
||||
from datetime import timedelta
|
||||
import enum
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from aioasuswrt.asuswrt import AsusWrt
|
||||
|
||||
from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from . import DATA_ASUSWRT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPLOAD_ICON = "mdi:upload-network"
|
||||
DOWNLOAD_ICON = "mdi:download-network"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
@enum.unique
|
||||
class _SensorTypes(enum.Enum):
|
||||
DEVICES = "devices"
|
||||
UPLOAD = "upload"
|
||||
DOWNLOAD = "download"
|
||||
DOWNLOAD_SPEED = "download_speed"
|
||||
UPLOAD_SPEED = "upload_speed"
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> Optional[str]:
|
||||
"""Return a string with the unit of the sensortype."""
|
||||
if self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD):
|
||||
return DATA_GIGABYTES
|
||||
if self in (_SensorTypes.UPLOAD_SPEED, _SensorTypes.DOWNLOAD_SPEED):
|
||||
return DATA_RATE_MEGABITS_PER_SECOND
|
||||
return None
|
||||
|
||||
@property
|
||||
def icon(self) -> Optional[str]:
|
||||
"""Return the expected icon for the sensortype."""
|
||||
if self in (_SensorTypes.UPLOAD, _SensorTypes.UPLOAD_SPEED):
|
||||
return UPLOAD_ICON
|
||||
if self in (_SensorTypes.DOWNLOAD, _SensorTypes.DOWNLOAD_SPEED):
|
||||
return DOWNLOAD_ICON
|
||||
return None
|
||||
|
||||
@property
|
||||
def sensor_name(self) -> Optional[str]:
|
||||
"""Return the name of the sensor."""
|
||||
if self is _SensorTypes.DEVICES:
|
||||
return "Asuswrt Devices Connected"
|
||||
if self is _SensorTypes.UPLOAD:
|
||||
return "Asuswrt Upload"
|
||||
if self is _SensorTypes.DOWNLOAD:
|
||||
return "Asuswrt Download"
|
||||
if self is _SensorTypes.UPLOAD_SPEED:
|
||||
return "Asuswrt Upload Speed"
|
||||
if self is _SensorTypes.DOWNLOAD_SPEED:
|
||||
return "Asuswrt Download Speed"
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_speed(self) -> bool:
|
||||
"""Return True if the type is an upload/download speed."""
|
||||
return self in (_SensorTypes.UPLOAD_SPEED, _SensorTypes.DOWNLOAD_SPEED)
|
||||
|
||||
@property
|
||||
def is_size(self) -> bool:
|
||||
"""Return True if the type is the total upload/download size."""
|
||||
return self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the asuswrt sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
api = hass.data[DATA_ASUSWRT]
|
||||
api: AsusWrt = hass.data[DATA_ASUSWRT]
|
||||
|
||||
devices = []
|
||||
# Let's discover the valid sensor types.
|
||||
sensors = [_SensorTypes(x) for x in discovery_info]
|
||||
|
||||
if "devices" in discovery_info:
|
||||
devices.append(AsuswrtDevicesSensor(api))
|
||||
if "download" in discovery_info:
|
||||
devices.append(AsuswrtTotalRXSensor(api))
|
||||
if "upload" in discovery_info:
|
||||
devices.append(AsuswrtTotalTXSensor(api))
|
||||
if "download_speed" in discovery_info:
|
||||
devices.append(AsuswrtRXSensor(api))
|
||||
if "upload_speed" in discovery_info:
|
||||
devices.append(AsuswrtTXSensor(api))
|
||||
data_handler = AsuswrtDataHandler(sensors, api)
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="sensor",
|
||||
update_method=data_handler.update_data,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
|
||||
add_entities(devices)
|
||||
await coordinator.async_refresh()
|
||||
async_add_entities([AsuswrtSensor(coordinator, x) for x in sensors])
|
||||
|
||||
|
||||
class AsuswrtSensor(Entity):
|
||||
"""Representation of a asuswrt sensor."""
|
||||
class AsuswrtDataHandler:
|
||||
"""Class handling the API updates."""
|
||||
|
||||
_name = "generic"
|
||||
|
||||
def __init__(self, api: AsusWrt):
|
||||
"""Initialize the sensor."""
|
||||
def __init__(self, sensors: List[_SensorTypes], api: AsusWrt):
|
||||
"""Initialize the handler class."""
|
||||
self._api = api
|
||||
self._state = None
|
||||
self._devices = None
|
||||
self._rates = None
|
||||
self._speed = None
|
||||
self._connect_error = False
|
||||
self._sensors = sensors
|
||||
self._connected = True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
async def update_data(self) -> Dict[_SensorTypes, Any]:
|
||||
"""Fetch the relevant data from the router."""
|
||||
ret_dict: Dict[_SensorTypes, Any] = {}
|
||||
try:
|
||||
if _SensorTypes.DEVICES in self._sensors:
|
||||
# Let's check the nr of devices.
|
||||
devices = await self._api.async_get_connected_devices()
|
||||
ret_dict[_SensorTypes.DEVICES] = len(devices)
|
||||
|
||||
if any(x.is_speed for x in self._sensors):
|
||||
# Let's check the upload and download speed
|
||||
speed = await self._api.async_get_current_transfer_rates()
|
||||
ret_dict[_SensorTypes.DOWNLOAD_SPEED] = round(speed[0] / 125000, 2)
|
||||
ret_dict[_SensorTypes.UPLOAD_SPEED] = round(speed[1] / 125000, 2)
|
||||
|
||||
if any(x.is_size for x in self._sensors):
|
||||
rates = await self._api.async_get_bytes_total()
|
||||
ret_dict[_SensorTypes.DOWNLOAD] = round(rates[0] / 1000000000, 1)
|
||||
ret_dict[_SensorTypes.UPLOAD] = round(rates[1] / 1000000000, 1)
|
||||
|
||||
if not self._connected:
|
||||
# Log a successful reconnect
|
||||
self._connected = True
|
||||
_LOGGER.warning("Successfully reconnected to ASUS router")
|
||||
|
||||
except OSError as err:
|
||||
if self._connected:
|
||||
# Log the first time connection was lost
|
||||
_LOGGER.warning("Lost connection to router error due to: '%s'", err)
|
||||
self._connected = False
|
||||
|
||||
return ret_dict
|
||||
|
||||
|
||||
class AsuswrtSensor(CoordinatorEntity):
|
||||
"""The asuswrt specific sensor class."""
|
||||
|
||||
def __init__(self, coordinator: DataUpdateCoordinator, sensor_type: _SensorTypes):
|
||||
"""Initialize the sensor class."""
|
||||
super().__init__(coordinator)
|
||||
self._type = sensor_type
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch status from asuswrt."""
|
||||
try:
|
||||
self._devices = await self._api.async_get_connected_devices()
|
||||
self._rates = await self._api.async_get_bytes_total()
|
||||
self._speed = await self._api.async_get_current_transfer_rates()
|
||||
if self._connect_error:
|
||||
self._connect_error = False
|
||||
_LOGGER.info("Reconnected to ASUS router for %s update", self.entity_id)
|
||||
except OSError as err:
|
||||
if not self._connect_error:
|
||||
self._connect_error = True
|
||||
_LOGGER.error(
|
||||
"Error connecting to ASUS router for %s update: %s",
|
||||
self.entity_id,
|
||||
err,
|
||||
)
|
||||
|
||||
|
||||
class AsuswrtDevicesSensor(AsuswrtSensor):
|
||||
"""Representation of a asuswrt download speed sensor."""
|
||||
|
||||
_name = "Asuswrt Devices Connected"
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch new state data for the sensor."""
|
||||
await super().async_update()
|
||||
if self._devices:
|
||||
self._state = len(self._devices)
|
||||
|
||||
|
||||
class AsuswrtRXSensor(AsuswrtSensor):
|
||||
"""Representation of a asuswrt download speed sensor."""
|
||||
|
||||
_name = "Asuswrt Download Speed"
|
||||
_unit = DATA_RATE_MEGABITS_PER_SECOND
|
||||
return self.coordinator.data.get(self._type)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return DOWNLOAD_ICON
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
return self._type.sensor_name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch new state data for the sensor."""
|
||||
await super().async_update()
|
||||
if self._speed:
|
||||
self._state = round(self._speed[0] / 125000, 2)
|
||||
|
||||
|
||||
class AsuswrtTXSensor(AsuswrtSensor):
|
||||
"""Representation of a asuswrt upload speed sensor."""
|
||||
|
||||
_name = "Asuswrt Upload Speed"
|
||||
_unit = DATA_RATE_MEGABITS_PER_SECOND
|
||||
def icon(self) -> Optional[str]:
|
||||
"""Return the icon to use in the frontend."""
|
||||
return self._type.icon
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return UPLOAD_ICON
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch new state data for the sensor."""
|
||||
await super().async_update()
|
||||
if self._speed:
|
||||
self._state = round(self._speed[1] / 125000, 2)
|
||||
|
||||
|
||||
class AsuswrtTotalRXSensor(AsuswrtSensor):
|
||||
"""Representation of a asuswrt total download sensor."""
|
||||
|
||||
_name = "Asuswrt Download"
|
||||
_unit = DATA_GIGABYTES
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return DOWNLOAD_ICON
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch new state data for the sensor."""
|
||||
await super().async_update()
|
||||
if self._rates:
|
||||
self._state = round(self._rates[0] / 1000000000, 1)
|
||||
|
||||
|
||||
class AsuswrtTotalTXSensor(AsuswrtSensor):
|
||||
"""Representation of a asuswrt total upload sensor."""
|
||||
|
||||
_name = "Asuswrt Upload"
|
||||
_unit = DATA_GIGABYTES
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return UPLOAD_ICON
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch new state data for the sensor."""
|
||||
await super().async_update()
|
||||
if self._rates:
|
||||
self._state = round(self._rates[1] / 1000000000, 1)
|
||||
def unit_of_measurement(self) -> Optional[str]:
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._type.unit_of_measurement
|
||||
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
7
homeassistant/components/atag/translations/ka.json
Normal file
7
homeassistant/components/atag/translations/ka.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1"
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Konto on juba seadistatud",
|
||||
"reauth_successful": "Taasautentimine \u00f5nnestus"
|
||||
"reauth_successful": "Taastuvastamine \u00f5nnestus"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "\u00dchendamine nurjus",
|
||||
|
@ -1 +1,130 @@
|
||||
"""The aurora component."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from auroranoaa import AuroraForecast
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
AURORA_API,
|
||||
CONF_THRESHOLD,
|
||||
COORDINATOR,
|
||||
DEFAULT_POLLING_INTERVAL,
|
||||
DEFAULT_THRESHOLD,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = ["binary_sensor"]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the Aurora component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Aurora from a config entry."""
|
||||
|
||||
conf = entry.data
|
||||
options = entry.options
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
api = AuroraForecast(session)
|
||||
|
||||
longitude = conf[CONF_LONGITUDE]
|
||||
latitude = conf[CONF_LATITUDE]
|
||||
polling_interval = DEFAULT_POLLING_INTERVAL
|
||||
threshold = options.get(CONF_THRESHOLD, DEFAULT_THRESHOLD)
|
||||
name = conf[CONF_NAME]
|
||||
|
||||
coordinator = AuroraDataUpdateCoordinator(
|
||||
hass=hass,
|
||||
name=name,
|
||||
polling_interval=polling_interval,
|
||||
api=api,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
threshold=threshold,
|
||||
)
|
||||
|
||||
await coordinator.async_refresh()
|
||||
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
COORDINATOR: coordinator,
|
||||
AURORA_API: api,
|
||||
}
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class AuroraDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching data from the NOAA Aurora API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
name: str,
|
||||
polling_interval: int,
|
||||
api: str,
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
threshold: float,
|
||||
):
|
||||
"""Initialize the data updater."""
|
||||
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=_LOGGER,
|
||||
name=name,
|
||||
update_interval=timedelta(minutes=polling_interval),
|
||||
)
|
||||
|
||||
self.api = api
|
||||
self.name = name
|
||||
self.latitude = int(latitude)
|
||||
self.longitude = int(longitude)
|
||||
self.threshold = int(threshold)
|
||||
|
||||
async def _async_update_data(self):
|
||||
"""Fetch the data from the NOAA Aurora Forecast."""
|
||||
|
||||
try:
|
||||
return await self.api.get_forecast_data(self.longitude, self.latitude)
|
||||
except ConnectionError as error:
|
||||
raise UpdateFailed(f"Error updating from NOAA: {error}") from error
|
||||
|
@ -1,146 +1,75 @@
|
||||
"""Support for aurora forecast data sensor."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from math import floor
|
||||
|
||||
from aiohttp.hdrs import USER_AGENT
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
from . import AuroraDataUpdateCoordinator
|
||||
from .const import (
|
||||
ATTR_IDENTIFIERS,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTRIBUTION,
|
||||
COORDINATOR,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric Administration"
|
||||
CONF_THRESHOLD = "forecast_threshold"
|
||||
|
||||
DEFAULT_DEVICE_CLASS = "visible"
|
||||
DEFAULT_NAME = "Aurora Visibility"
|
||||
DEFAULT_THRESHOLD = 75
|
||||
async def async_setup_entry(hass, entry, async_add_entries):
|
||||
"""Set up the binary_sensor platform."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
|
||||
name = coordinator.name
|
||||
|
||||
HA_USER_AGENT = "Home Assistant Aurora Tracker v.0.1.0"
|
||||
entity = AuroraSensor(coordinator, name)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
|
||||
URL = "http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int,
|
||||
}
|
||||
)
|
||||
async_add_entries([entity])
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the aurora sensor."""
|
||||
if None in (hass.config.latitude, hass.config.longitude):
|
||||
_LOGGER.error("Lat. or long. not set in Home Assistant config")
|
||||
return False
|
||||
|
||||
name = config[CONF_NAME]
|
||||
threshold = config[CONF_THRESHOLD]
|
||||
|
||||
try:
|
||||
aurora_data = AuroraData(hass.config.latitude, hass.config.longitude, threshold)
|
||||
aurora_data.update()
|
||||
except requests.exceptions.HTTPError as error:
|
||||
_LOGGER.error("Connection to aurora forecast service failed: %s", error)
|
||||
return False
|
||||
|
||||
add_entities([AuroraSensor(aurora_data, name)], True)
|
||||
|
||||
|
||||
class AuroraSensor(BinarySensorEntity):
|
||||
class AuroraSensor(CoordinatorEntity, BinarySensorEntity):
|
||||
"""Implementation of an aurora sensor."""
|
||||
|
||||
def __init__(self, aurora_data, name):
|
||||
"""Initialize the sensor."""
|
||||
self.aurora_data = aurora_data
|
||||
def __init__(self, coordinator: AuroraDataUpdateCoordinator, name):
|
||||
"""Define the binary sensor for the Aurora integration."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
|
||||
self._name = name
|
||||
self.coordinator = coordinator
|
||||
self._unique_id = f"{self.coordinator.latitude}_{self.coordinator.longitude}"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Define the unique id based on the latitude and longitude."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self._name}"
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if aurora is visible."""
|
||||
return self.aurora_data.is_visible if self.aurora_data else False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device."""
|
||||
return DEFAULT_DEVICE_CLASS
|
||||
return self.coordinator.data > self.coordinator.threshold
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {}
|
||||
return {"attribution": ATTRIBUTION}
|
||||
|
||||
if self.aurora_data:
|
||||
attrs["visibility_level"] = self.aurora_data.visibility_level
|
||||
attrs["message"] = self.aurora_data.is_visible_text
|
||||
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
|
||||
return attrs
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon for the sensor."""
|
||||
return "mdi:hazard-lights"
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from Aurora API and updates the states."""
|
||||
self.aurora_data.update()
|
||||
|
||||
|
||||
class AuroraData:
|
||||
"""Get aurora forecast."""
|
||||
|
||||
def __init__(self, latitude, longitude, threshold):
|
||||
"""Initialize the data object."""
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.headers = {USER_AGENT: HA_USER_AGENT}
|
||||
self.threshold = int(threshold)
|
||||
self.is_visible = None
|
||||
self.is_visible_text = None
|
||||
self.visibility_level = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from the Aurora service."""
|
||||
try:
|
||||
self.visibility_level = self.get_aurora_forecast()
|
||||
if int(self.visibility_level) > self.threshold:
|
||||
self.is_visible = True
|
||||
self.is_visible_text = "visible!"
|
||||
else:
|
||||
self.is_visible = False
|
||||
self.is_visible_text = "nothing's out"
|
||||
|
||||
except requests.exceptions.HTTPError as error:
|
||||
_LOGGER.error("Connection to aurora forecast service failed: %s", error)
|
||||
return False
|
||||
|
||||
def get_aurora_forecast(self):
|
||||
"""Get forecast data and parse for given long/lat."""
|
||||
raw_data = requests.get(URL, headers=self.headers, timeout=5).text
|
||||
# We discard comment rows (#)
|
||||
# We split the raw text by line (\n)
|
||||
# For each line we trim leading spaces and split by spaces
|
||||
forecast_table = [
|
||||
row.strip().split()
|
||||
for row in raw_data.split("\n")
|
||||
if not row.startswith("#")
|
||||
]
|
||||
|
||||
# Convert lat and long for data points in table
|
||||
# Assumes self.latitude belongs to [-90;90[ (South to North)
|
||||
# Assumes self.longitude belongs to [-180;180[ (West to East)
|
||||
# No assumptions made regarding the number of rows and columns
|
||||
converted_latitude = floor((self.latitude + 90) * len(forecast_table) / 180)
|
||||
converted_longitude = floor(
|
||||
(self.longitude + 180) * len(forecast_table[converted_latitude]) / 360
|
||||
)
|
||||
|
||||
return forecast_table[converted_latitude][converted_longitude]
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Define the device based on name."""
|
||||
return {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)},
|
||||
ATTR_NAME: self.coordinator.name,
|
||||
ATTR_MANUFACTURER: "NOAA",
|
||||
ATTR_MODEL: "Aurora Visibility Sensor",
|
||||
}
|
||||
|
110
homeassistant/components/aurora/config_flow.py
Normal file
110
homeassistant/components/aurora/config_flow.py
Normal file
@ -0,0 +1,110 @@
|
||||
"""Config flow for SpaceX Launches and Starman."""
|
||||
import logging
|
||||
|
||||
from auroranoaa import AuroraForecast
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import CONF_THRESHOLD, DEFAULT_NAME, DEFAULT_THRESHOLD, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for NOAA Aurora Integration."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
name = user_input[CONF_NAME]
|
||||
longitude = user_input[CONF_LONGITUDE]
|
||||
latitude = user_input[CONF_LATITUDE]
|
||||
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
api = AuroraForecast(session=session)
|
||||
|
||||
try:
|
||||
await api.get_forecast_data(longitude, latitude)
|
||||
except ConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(
|
||||
f"{DOMAIN}_{user_input[CONF_LONGITUDE]}_{user_input[CONF_LATITUDE]}"
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=f"Aurora - {name}", data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
|
||||
vol.Required(
|
||||
CONF_LONGITUDE,
|
||||
default=self.hass.config.longitude,
|
||||
): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=-180, max=180),
|
||||
),
|
||||
vol.Required(
|
||||
CONF_LATITUDE,
|
||||
default=self.hass.config.latitude,
|
||||
): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=-90, max=90),
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle options flow changes."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage options."""
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_THRESHOLD,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_THRESHOLD, DEFAULT_THRESHOLD
|
||||
),
|
||||
): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=0, max=100),
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
13
homeassistant/components/aurora/const.py
Normal file
13
homeassistant/components/aurora/const.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""Constants for the Aurora integration."""
|
||||
|
||||
DOMAIN = "aurora"
|
||||
COORDINATOR = "coordinator"
|
||||
AURORA_API = "aurora_api"
|
||||
ATTR_IDENTIFIERS = "identifiers"
|
||||
ATTR_MANUFACTURER = "manufacturer"
|
||||
ATTR_MODEL = "model"
|
||||
DEFAULT_POLLING_INTERVAL = 5
|
||||
CONF_THRESHOLD = "forecast_threshold"
|
||||
DEFAULT_THRESHOLD = 75
|
||||
ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric Administration"
|
||||
DEFAULT_NAME = "Aurora Visibility"
|
@ -2,5 +2,7 @@
|
||||
"domain": "aurora",
|
||||
"name": "Aurora",
|
||||
"documentation": "https://www.home-assistant.io/integrations/aurora",
|
||||
"codeowners": []
|
||||
"config_flow": true,
|
||||
"codeowners": ["@djtimca"],
|
||||
"requirements": ["auroranoaa==0.0.2"]
|
||||
}
|
||||
|
26
homeassistant/components/aurora/strings.json
Normal file
26
homeassistant/components/aurora/strings.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"title": "NOAA Aurora Sensor",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"longitude": "[%key:common::config_flow::data::longitude%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"threshold": "Threshold (%)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
homeassistant/components/aurora/translations/en.json
Normal file
26
homeassistant/components/aurora/translations/en.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "Name"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"threshold": "Threshold (%)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "NOAA Aurora Sensor"
|
||||
}
|
26
homeassistant/components/aurora/translations/hu.json
Normal file
26
homeassistant/components/aurora/translations/hu.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Nem siker\u00fclt csatlakozni"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"latitude": "Sz\u00e9less\u00e9g",
|
||||
"longitude": "Hossz\u00fas\u00e1g",
|
||||
"name": "N\u00e9v"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"threshold": "K\u00fcsz\u00f6b (%)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Nemzeti \u00d3ce\u00e1n- \u00e9s L\u00e9gk\u00f6rkutat\u00e1si Hivatal (NOAA) Aurora \u00e9rz\u00e9kel\u0151"
|
||||
}
|
26
homeassistant/components/aurora/translations/it.json
Normal file
26
homeassistant/components/aurora/translations/it.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"latitude": "Latitudine",
|
||||
"longitude": "Logitudine",
|
||||
"name": "Nome"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"threshold": "Soglia (%)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Sensore NOAA Aurora"
|
||||
}
|
26
homeassistant/components/aurora/translations/ka.json
Normal file
26
homeassistant/components/aurora/translations/ka.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8 \u10d5\u10d4\u10e0 \u10d3\u10d0\u10db\u10e7\u10d0\u10e0\u10d3\u10d0"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"latitude": "\u10d2\u10d0\u10dc\u10d4\u10d3\u10d8",
|
||||
"longitude": "\u10d2\u10e0\u10eb\u10d4\u10d3\u10d8",
|
||||
"name": "\u10e1\u10d0\u10ee\u10d4\u10da\u10d8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"threshold": "\u10d6\u10e6\u10d5\u10d0\u10e0\u10d8 (%)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "NOAA Aurora \u10e1\u10d4\u10dc\u10e1\u10dd\u10e0\u10d8"
|
||||
}
|
26
homeassistant/components/aurora/translations/lb.json
Normal file
26
homeassistant/components/aurora/translations/lb.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Feeler beim verbannen"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"latitude": "L\u00e4ngregraad",
|
||||
"longitude": "Breedegrad",
|
||||
"name": "Numm"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"threshold": "Grenzw\u00e4ert (%)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "NOAA Aurora Sensor"
|
||||
}
|
13
homeassistant/components/aurora/translations/pt.json
Normal file
13
homeassistant/components/aurora/translations/pt.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"name": "Nome"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
homeassistant/components/aurora/translations/sl.json
Normal file
11
homeassistant/components/aurora/translations/sl.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "Ime"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
homeassistant/components/aurora/translations/zh-Hans.json
Normal file
11
homeassistant/components/aurora/translations/zh-Hans.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "\u540d\u79f0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
homeassistant/components/aurora/translations/zh-Hant.json
Normal file
26
homeassistant/components/aurora/translations/zh-Hant.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "\u9023\u7dda\u5931\u6557"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"latitude": "\u7def\u5ea6",
|
||||
"longitude": "\u7d93\u5ea6",
|
||||
"name": "\u540d\u7a31"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"threshold": "\u95a5\u503c (%)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "NOAA Aurora \u50b3\u611f\u5668"
|
||||
}
|
@ -43,40 +43,37 @@ from homeassistant.helpers.script import (
|
||||
ATTR_MODE,
|
||||
CONF_MAX,
|
||||
CONF_MAX_EXCEEDED,
|
||||
SCRIPT_MODE_SINGLE,
|
||||
Script,
|
||||
make_script_schema,
|
||||
)
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.trigger import async_initialize_triggers
|
||||
from homeassistant.helpers.typing import TemplateVarsType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.dt import parse_datetime
|
||||
|
||||
# Not used except by packages to check config structure
|
||||
from .config import PLATFORM_SCHEMA # noqa
|
||||
from .config import async_validate_config_item
|
||||
from .const import (
|
||||
CONF_ACTION,
|
||||
CONF_CONDITION,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_TRIGGER,
|
||||
DEFAULT_INITIAL_STATE,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
from .helpers import async_get_blueprints
|
||||
|
||||
# mypy: allow-untyped-calls, allow-untyped-defs
|
||||
# mypy: no-check-untyped-defs, no-warn-return-any
|
||||
|
||||
DOMAIN = "automation"
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
DATA_BLUEPRINTS = "automation_blueprints"
|
||||
|
||||
CONF_DESCRIPTION = "description"
|
||||
CONF_HIDE_ENTITY = "hide_entity"
|
||||
|
||||
CONF_CONDITION = "condition"
|
||||
CONF_ACTION = "action"
|
||||
CONF_TRIGGER = "trigger"
|
||||
CONF_CONDITION_TYPE = "condition_type"
|
||||
CONF_INITIAL_STATE = "initial_state"
|
||||
CONF_SKIP_CONDITION = "skip_condition"
|
||||
CONF_STOP_ACTIONS = "stop_actions"
|
||||
CONF_BLUEPRINT = "blueprint"
|
||||
CONF_INPUT = "input"
|
||||
|
||||
DEFAULT_INITIAL_STATE = True
|
||||
DEFAULT_STOP_ACTIONS = True
|
||||
|
||||
EVENT_AUTOMATION_RELOADED = "automation_reloaded"
|
||||
@ -87,38 +84,8 @@ ATTR_SOURCE = "source"
|
||||
ATTR_VARIABLES = "variables"
|
||||
SERVICE_TRIGGER = "trigger"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]]
|
||||
|
||||
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_HIDE_ENTITY, invalidation_version="0.110"),
|
||||
make_script_schema(
|
||||
{
|
||||
# str on purpose
|
||||
CONF_ID: str,
|
||||
CONF_ALIAS: cv.string,
|
||||
vol.Optional(CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
|
||||
vol.Optional(CONF_HIDE_ENTITY): cv.boolean,
|
||||
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
|
||||
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
|
||||
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
|
||||
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
},
|
||||
SCRIPT_MODE_SINGLE,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@singleton(DATA_BLUEPRINTS)
|
||||
@callback
|
||||
def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: # type: ignore
|
||||
"""Get automation blueprints."""
|
||||
return blueprint.DomainBlueprints(hass, DOMAIN, _LOGGER) # type: ignore
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass, entity_id):
|
||||
@ -194,9 +161,13 @@ def devices_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]:
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the automation."""
|
||||
hass.data[DOMAIN] = component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
|
||||
|
||||
await _async_process_config(hass, config, component)
|
||||
# To register the automation blueprints
|
||||
async_get_blueprints(hass)
|
||||
|
||||
if not await _async_process_config(hass, config, component):
|
||||
await async_get_blueprints(hass).async_populate()
|
||||
|
||||
async def trigger_service_handler(entity, service_call):
|
||||
"""Handle automation triggers."""
|
||||
@ -263,7 +234,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
self._is_enabled = False
|
||||
self._referenced_entities: Optional[Set[str]] = None
|
||||
self._referenced_devices: Optional[Set[str]] = None
|
||||
self._logger = _LOGGER
|
||||
self._logger = LOGGER
|
||||
self._variables: ScriptVariables = variables
|
||||
|
||||
@property
|
||||
@ -517,12 +488,13 @@ async def _async_process_config(
|
||||
hass: HomeAssistant,
|
||||
config: Dict[str, Any],
|
||||
component: EntityComponent,
|
||||
) -> None:
|
||||
) -> bool:
|
||||
"""Process config and add automations.
|
||||
|
||||
This method is a coroutine.
|
||||
Returns if blueprints were used.
|
||||
"""
|
||||
entities = []
|
||||
blueprints_used = False
|
||||
|
||||
for config_key in extract_domain_configs(config, DOMAIN):
|
||||
conf: List[Union[Dict[str, Any], blueprint.BlueprintInputs]] = config[ # type: ignore
|
||||
@ -531,15 +503,18 @@ async def _async_process_config(
|
||||
|
||||
for list_no, config_block in enumerate(conf):
|
||||
if isinstance(config_block, blueprint.BlueprintInputs): # type: ignore
|
||||
blueprints_used = True
|
||||
blueprint_inputs = config_block
|
||||
|
||||
try:
|
||||
config_block = cast(
|
||||
Dict[str, Any],
|
||||
PLATFORM_SCHEMA(blueprint_inputs.async_substitute()),
|
||||
await async_validate_config_item(
|
||||
hass, blueprint_inputs.async_substitute()
|
||||
),
|
||||
)
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error(
|
||||
LOGGER.error(
|
||||
"Blueprint %s generated invalid automation with inputs %s: %s",
|
||||
blueprint_inputs.blueprint.name,
|
||||
blueprint_inputs.inputs,
|
||||
@ -561,7 +536,7 @@ async def _async_process_config(
|
||||
script_mode=config_block[CONF_MODE],
|
||||
max_runs=config_block[CONF_MAX],
|
||||
max_exceeded=config_block[CONF_MAX_EXCEEDED],
|
||||
logger=_LOGGER,
|
||||
logger=LOGGER,
|
||||
# We don't pass variables here
|
||||
# Automation will already render them to use them in the condition
|
||||
# and so will pass them on to the script.
|
||||
@ -590,6 +565,8 @@ async def _async_process_config(
|
||||
if entities:
|
||||
await component.async_add_entities(entities)
|
||||
|
||||
return blueprints_used
|
||||
|
||||
|
||||
async def _async_process_if(hass, config, p_config):
|
||||
"""Process if checks."""
|
||||
@ -600,7 +577,7 @@ async def _async_process_if(hass, config, p_config):
|
||||
try:
|
||||
checks.append(await condition.async_from_config(hass, if_config, False))
|
||||
except HomeAssistantError as ex:
|
||||
_LOGGER.warning("Invalid condition: %s", ex)
|
||||
LOGGER.warning("Invalid condition: %s", ex)
|
||||
return None
|
||||
|
||||
def if_action(variables=None):
|
||||
|
@ -0,0 +1,50 @@
|
||||
blueprint:
|
||||
name: Motion-activated Light
|
||||
description: Turn on a light when motion is detected.
|
||||
domain: automation
|
||||
source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml
|
||||
input:
|
||||
motion_entity:
|
||||
name: Motion Sensor
|
||||
selector:
|
||||
entity:
|
||||
domain: binary_sensor
|
||||
device_class: motion
|
||||
light_target:
|
||||
name: Light
|
||||
selector:
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
no_motion_wait:
|
||||
name: Wait time
|
||||
description: Time to leave the light on after last motion is detected.
|
||||
default: 120
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 3600
|
||||
unit_of_measurement: seconds
|
||||
|
||||
# If motion is detected within the delay,
|
||||
# we restart the script.
|
||||
mode: restart
|
||||
max_exceeded: silent
|
||||
|
||||
trigger:
|
||||
platform: state
|
||||
entity_id: !input motion_entity
|
||||
from: "off"
|
||||
to: "on"
|
||||
|
||||
action:
|
||||
- service: light.turn_on
|
||||
target: !input light_target
|
||||
- wait_for_trigger:
|
||||
platform: state
|
||||
entity_id: !input motion_entity
|
||||
from: "on"
|
||||
to: "off"
|
||||
- delay: !input no_motion_wait
|
||||
- service: light.turn_off
|
||||
target: !input light_target
|
@ -0,0 +1,43 @@
|
||||
blueprint:
|
||||
name: Zone Notification
|
||||
description: Send a notification to a device when a person leaves a specific zone.
|
||||
domain: automation
|
||||
source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
|
||||
input:
|
||||
person_entity:
|
||||
name: Person
|
||||
selector:
|
||||
entity:
|
||||
domain: person
|
||||
zone_entity:
|
||||
name: Zone
|
||||
selector:
|
||||
entity:
|
||||
domain: zone
|
||||
notify_device:
|
||||
name: Device to notify
|
||||
description: Device needs to run the official Home Assistant app to receive notifications.
|
||||
selector:
|
||||
device:
|
||||
integration: mobile_app
|
||||
|
||||
trigger:
|
||||
platform: state
|
||||
entity_id: !input person_entity
|
||||
|
||||
variables:
|
||||
zone_entity: !input zone_entity
|
||||
# This is the state of the person when it's in this zone.
|
||||
zone_state: "{{ states[zone_entity].name }}"
|
||||
person_entity: !input person_entity
|
||||
person_name: "{{ states[person_entity].name }}"
|
||||
|
||||
condition:
|
||||
condition: template
|
||||
value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}"
|
||||
|
||||
action:
|
||||
domain: mobile_app
|
||||
type: notify
|
||||
device_id: !input notify_device
|
||||
message: "{{ person_name }} has left {{ zone_state }}"
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user