Merge pull request #44175 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2020-12-13 22:21:47 +01:00 committed by GitHub
commit 3600dec6e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1344 changed files with 33458 additions and 7964 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,5 +31,11 @@
}
}
}
},
"system_health": {
"info": {
"can_reach_server": "Reach AccuWeather server",
"remaining_requests": "Remaining allowed requests"
}
}
}

View 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,
}

View File

@ -31,5 +31,11 @@
"title": "AccuWeather Options"
}
}
},
"system_health": {
"info": {
"can_reach_server": "Reach AccuWeather server",
"remaining_requests": "Remaining allowed requests"
}
}
}

View File

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

View File

@ -31,5 +31,11 @@
"title": "AccuWeatheri valikud"
}
}
},
"system_health": {
"info": {
"can_reach_server": "\u00dchendu Accuweatheri serveriga",
"remaining_requests": "Lubatud taotlusi on j\u00e4\u00e4nud"
}
}
}

View File

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

View File

@ -6,7 +6,7 @@
"step": {
"user": {
"data": {
"id": "Verts-ID"
"id": "Vert ID"
},
"title": "Velg en hub du vil legge til"
}

View File

@ -1,5 +1,8 @@
{
"config": {
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"step": {
"user": {
"data": {

View File

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

View File

@ -0,0 +1,9 @@
{
"config": {
"step": {
"user": {
"title": "Conectar"
}
}
}
}

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

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

View File

@ -0,0 +1,9 @@
{
"config": {
"step": {
"user": {
"title": "Pove\u017eite se"
}
}
}
}

View File

@ -1,5 +1,8 @@
{
"config": {
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"step": {
"user": {
"data": {

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

View File

@ -19,5 +19,10 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
}
},
"system_health": {
"info": {
"can_reach_server": "Reach Airly server"
}
}
}

View 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
)
}

View File

@ -19,5 +19,10 @@
"title": "Airly"
}
}
},
"system_health": {
"info": {
"can_reach_server": "Reach Airly server"
}
}
}

View File

@ -19,5 +19,10 @@
"title": ""
}
}
},
"system_health": {
"info": {
"can_reach_server": "\u00dchendu Airly serveriga"
}
}
}

View File

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

View File

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

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
{
"config": {
"create_entry": {
"default": "Sikeres csatlakoz\u00e1s az AlarmDecoderhez."
},
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
}
}
}

View File

@ -0,0 +1,7 @@
{
"config": {
"abort": {
"already_configured": "Naprava je \u017ee nastavljena"
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
{
"config": {
"create_entry": {
"default": "Sikeres autentik\u00e1ci\u00f3"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View 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": ""
}

View File

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

View File

@ -0,0 +1,7 @@
{
"config": {
"abort": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
{
"config": {
"error": {
"cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"step": {
"user": {
"data": {

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

View File

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

View File

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

View File

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

View 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),
),
}
),
)

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

View File

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

View 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 (%)"
}
}
}
}
}

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

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

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

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

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

View File

@ -0,0 +1,13 @@
{
"config": {
"step": {
"user": {
"data": {
"latitude": "Latitude",
"longitude": "Longitude",
"name": "Nome"
}
}
}
}
}

View File

@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"data": {
"name": "Ime"
}
}
}
}
}

View File

@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"data": {
"name": "\u540d\u79f0"
}
}
}
}
}

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

View File

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

View File

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

View File

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