Merge pull request #37280 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2020-07-01 16:47:17 +02:00 committed by GitHub
commit dc8bfb76dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1198 changed files with 36498 additions and 16033 deletions

View File

@ -46,7 +46,6 @@ omit =
homeassistant/components/android_ip_webcam/*
homeassistant/components/anel_pwrctrl/switch.py
homeassistant/components/anthemav/media_player.py
homeassistant/components/apache_kafka/*
homeassistant/components/apcupsd/*
homeassistant/components/apple_tv/*
homeassistant/components/aqualogic/*
@ -68,8 +67,8 @@ omit =
homeassistant/components/aurora_abb_powerone/sensor.py
homeassistant/components/avea/light.py
homeassistant/components/avion/light.py
homeassistant/components/avri/const.py
homeassistant/components/avri/sensor.py
homeassistant/components/azure_event_hub/*
homeassistant/components/azure_service_bus/*
homeassistant/components/baidu/tts.py
homeassistant/components/beewi_smartclim/sensor.py
@ -79,7 +78,12 @@ omit =
homeassistant/components/bh1750/sensor.py
homeassistant/components/bitcoin/sensor.py
homeassistant/components/bizkaibus/sensor.py
homeassistant/components/blink/*
homeassistant/components/blink/__init__.py
homeassistant/components/blink/alarm_control_panel.py
homeassistant/components/blink/binary_sensor.py
homeassistant/components/blink/camera.py
homeassistant/components/blink/const.py
homeassistant/components/blink/sensor.py
homeassistant/components/blinksticklight/light.py
homeassistant/components/blinkt/light.py
homeassistant/components/blockchain/sensor.py
@ -154,9 +158,14 @@ omit =
homeassistant/components/deluge/switch.py
homeassistant/components/denon/media_player.py
homeassistant/components/denonavr/media_player.py
homeassistant/components/denonavr/receiver.py
homeassistant/components/deutsche_bahn/sensor.py
homeassistant/components/devolo_home_control/__init__.py
homeassistant/components/devolo_home_control/binary_sensor.py
homeassistant/components/devolo_home_control/const.py
homeassistant/components/devolo_home_control/devolo_device.py
homeassistant/components/devolo_home_control/sensor.py
homeassistant/components/devolo_home_control/subscriber.py
homeassistant/components/devolo_home_control/switch.py
homeassistant/components/dht/sensor.py
homeassistant/components/digital_ocean/*
@ -255,7 +264,6 @@ omit =
homeassistant/components/folder_watcher/*
homeassistant/components/foobot/sensor.py
homeassistant/components/fortios/device_tracker.py
homeassistant/components/fortigate/*
homeassistant/components/foscam/camera.py
homeassistant/components/foscam/const.py
homeassistant/components/foursquare/*
@ -284,6 +292,7 @@ omit =
homeassistant/components/gitlab_ci/sensor.py
homeassistant/components/gitter/sensor.py
homeassistant/components/glances/__init__.py
homeassistant/components/glances/const.py
homeassistant/components/glances/sensor.py
homeassistant/components/gntp/notify.py
homeassistant/components/goalfeed/*
@ -339,6 +348,8 @@ omit =
homeassistant/components/hunterdouglas_powerview/sensor.py
homeassistant/components/hunterdouglas_powerview/cover.py
homeassistant/components/hunterdouglas_powerview/entity.py
homeassistant/components/hvv_departures/sensor.py
homeassistant/components/hvv_departures/__init__.py
homeassistant/components/hydrawise/*
homeassistant/components/hyperion/light.py
homeassistant/components/ialarm/alarm_control_panel.py
@ -431,7 +442,6 @@ omit =
homeassistant/components/linux_battery/sensor.py
homeassistant/components/lirc/*
homeassistant/components/llamalab_automate/notify.py
homeassistant/components/lockitron/lock.py
homeassistant/components/logi_circle/__init__.py
homeassistant/components/logi_circle/camera.py
homeassistant/components/logi_circle/const.py
@ -538,6 +548,7 @@ omit =
homeassistant/components/notion/sensor.py
homeassistant/components/noaa_tides/sensor.py
homeassistant/components/norway_air/air_quality.py
homeassistant/components/notify_events/notify.py
homeassistant/components/nsw_fuel_station/sensor.py
homeassistant/components/nuimo_controller/*
homeassistant/components/nuki/lock.py
@ -714,7 +725,11 @@ omit =
homeassistant/components/sinch/*
homeassistant/components/slide/*
homeassistant/components/sma/sensor.py
homeassistant/components/smappee/*
homeassistant/components/smappee/__init__.py
homeassistant/components/smappee/api.py
homeassistant/components/smappee/binary_sensor.py
homeassistant/components/smappee/sensor.py
homeassistant/components/smappee/switch.py
homeassistant/components/smarty/*
homeassistant/components/smarthab/*
homeassistant/components/sms/*
@ -740,7 +755,8 @@ omit =
homeassistant/components/spotcrime/sensor.py
homeassistant/components/spotify/__init__.py
homeassistant/components/spotify/media_player.py
homeassistant/components/squeezebox/*
homeassistant/components/squeezebox/__init__.py
homeassistant/components/squeezebox/media_player.py
homeassistant/components/starline/*
homeassistant/components/starlingbank/sensor.py
homeassistant/components/steam_online/sensor.py
@ -797,6 +813,7 @@ omit =
homeassistant/components/thomson/device_tracker.py
homeassistant/components/tibber/*
homeassistant/components/tikteck/light.py
homeassistant/components/tile/__init__.py
homeassistant/components/tile/device_tracker.py
homeassistant/components/time_date/sensor.py
homeassistant/components/tmb/sensor.py
@ -804,7 +821,16 @@ omit =
homeassistant/components/todoist/const.py
homeassistant/components/tof/sensor.py
homeassistant/components/tomato/device_tracker.py
homeassistant/components/toon/*
homeassistant/components/toon/__init__.py
homeassistant/components/toon/binary_sensor.py
homeassistant/components/toon/climate.py
homeassistant/components/toon/const.py
homeassistant/components/toon/coordinator.py
homeassistant/components/toon/helpers.py
homeassistant/components/toon/models.py
homeassistant/components/toon/oauth2.py
homeassistant/components/toon/sensor.py
homeassistant/components/toon/switch.py
homeassistant/components/torque/sensor.py
homeassistant/components/totalconnect/*
homeassistant/components/touchline/climate.py
@ -891,7 +917,14 @@ omit =
homeassistant/components/xeoma/camera.py
homeassistant/components/xfinity/device_tracker.py
homeassistant/components/xiaomi/camera.py
homeassistant/components/xiaomi_aqara/*
homeassistant/components/xiaomi_aqara/__init__.py
homeassistant/components/xiaomi_aqara/binary_sensor.py
homeassistant/components/xiaomi_aqara/const.py
homeassistant/components/xiaomi_aqara/cover.py
homeassistant/components/xiaomi_aqara/light.py
homeassistant/components/xiaomi_aqara/lock.py
homeassistant/components/xiaomi_aqara/sensor.py
homeassistant/components/xiaomi_aqara/switch.py
homeassistant/components/xiaomi_miio/__init__.py
homeassistant/components/xiaomi_miio/air_quality.py
homeassistant/components/xiaomi_miio/alarm_control_panel.py

2
.gitattributes vendored
View File

@ -8,3 +8,5 @@
*.png binary
*.zip binary
*.mp3 binary
Dockerfile.dev linguist-language=Dockerfile

View File

@ -21,7 +21,7 @@
- Home Assistant Core release with the issue:
- Last working Home Assistant Core release (if known):
- Operating environment (Home Assistant/Supervised/Docker/venv):
- Operating environment (OS/Container/Supervised/Core):
- Integration causing this issue:
- Link to integration documentation on our website:

View File

@ -25,7 +25,7 @@ about: Report an issue with Home Assistant Core
- Home Assistant Core release with the issue:
- Last working Home Assistant Core release (if known):
- Operating environment (Home Assistant/Supervised/Docker/venv):
- Operating environment (OS/Container/Supervised/Core):
- Integration causing this issue:
- Link to integration documentation on our website:

View File

@ -46,7 +46,7 @@ homeassistant/components/auth/* @home-assistant/core
homeassistant/components/automation/* @home-assistant/core
homeassistant/components/avea/* @pattyland
homeassistant/components/avri/* @timvancann
homeassistant/components/awair/* @danielsjf
homeassistant/components/awair/* @ahayworth @danielsjf
homeassistant/components/aws/* @awarecan @robbiet480
homeassistant/components/axis/* @Kane610
homeassistant/components/azure_event_hub/* @eavanvalkenburg
@ -57,7 +57,7 @@ homeassistant/components/bizkaibus/* @UgaitzEtxebarria
homeassistant/components/blebox/* @gadgetmobile
homeassistant/components/blink/* @fronzbot
homeassistant/components/bmp280/* @belidzs
homeassistant/components/bmw_connected_drive/* @gerard33
homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe
homeassistant/components/bom/* @maddenp
homeassistant/components/braviatv/* @robbiet480 @bieniu
homeassistant/components/broadlink/* @danielhiversen @felipediel
@ -86,6 +86,7 @@ homeassistant/components/cpuspeed/* @fabaff
homeassistant/components/cups/* @fabaff
homeassistant/components/daikin/* @fredrike
homeassistant/components/darksky/* @fabaff
homeassistant/components/debugpy/* @frenck
homeassistant/components/deconz/* @Kane610
homeassistant/components/delijn/* @bollewolle @Emilv2
homeassistant/components/demo/* @home-assistant/core
@ -133,7 +134,6 @@ homeassistant/components/flock/* @fabaff
homeassistant/components/flume/* @ChrisMandich @bdraco
homeassistant/components/flunearyou/* @bachya
homeassistant/components/forked_daapd/* @uvjustin
homeassistant/components/fortigate/* @kifeo
homeassistant/components/fortios/* @kimfrellsen
homeassistant/components/foscam/* @skgsergio
homeassistant/components/foursquare/* @robbiet480
@ -184,7 +184,10 @@ homeassistant/components/http/* @home-assistant/core
homeassistant/components/huawei_lte/* @scop @fphammerle
homeassistant/components/huawei_router/* @abmantis
homeassistant/components/hue/* @balloob
homeassistant/components/humidifier/* @home-assistant/core @Shulyaka
homeassistant/components/hunterdouglas_powerview/* @bdraco
homeassistant/components/hvv_departures/* @vigonotion
homeassistant/components/hydrawise/* @ptcryan
homeassistant/components/iammeter/* @lewei50
homeassistant/components/iaqualink/* @flz
homeassistant/components/icloud/* @Quentame
@ -243,6 +246,7 @@ homeassistant/components/melissa/* @kennedyshead
homeassistant/components/met/* @danielhiversen
homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame
homeassistant/components/meteoalarm/* @rolfberkenbosch
homeassistant/components/metoffice/* @MrHarcombe
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
homeassistant/components/mikrotik/* @engrbm87
homeassistant/components/mill/* @danielhiversen
@ -274,6 +278,7 @@ homeassistant/components/nissan_leaf/* @filcole
homeassistant/components/nmbs/* @thibmaek
homeassistant/components/no_ip/* @fabaff
homeassistant/components/notify/* @home-assistant/core
homeassistant/components/notify_events/* @matrozov @papajojo
homeassistant/components/notion/* @bachya
homeassistant/components/nsw_fuel_station/* @nickw444
homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte
@ -311,9 +316,10 @@ homeassistant/components/plaato/* @JohNan
homeassistant/components/plant/* @ChristianKuehnel
homeassistant/components/plex/* @jjlawren
homeassistant/components/plugwise/* @CoMPaTech @bouwew
homeassistant/components/plum_lightpad/* @ColinHarrington
homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa
homeassistant/components/point/* @fredrike
homeassistant/components/powerwall/* @bdraco @jrester
homeassistant/components/prometheus/* @knyar
homeassistant/components/proxmoxve/* @k4ds3 @jhollowe
homeassistant/components/ps4/* @ktnrg45
homeassistant/components/ptvsd/* @swamp-ig
@ -362,6 +368,7 @@ homeassistant/components/sinch/* @bendikrb
homeassistant/components/sisyphus/* @jkeljo
homeassistant/components/slide/* @ualex73
homeassistant/components/sma/* @kellerza
homeassistant/components/smappee/* @bsmappee
homeassistant/components/smarthab/* @outadoc
homeassistant/components/smartthings/* @andrewsayre
homeassistant/components/smarty/* @z0mbieprocess
@ -375,7 +382,7 @@ homeassistant/components/somfy/* @tetienne
homeassistant/components/sonarr/* @ctalkington
homeassistant/components/songpal/* @rytilahti @shenxn
homeassistant/components/spaceapi/* @fabaff
homeassistant/components/speedtestdotnet/* @rohankapoorcom
homeassistant/components/speedtestdotnet/* @rohankapoorcom @engrbm87
homeassistant/components/spider/* @peternijssen
homeassistant/components/spotify/* @frenck
homeassistant/components/sql/* @dgomes
@ -453,7 +460,6 @@ homeassistant/components/watson_tts/* @rutkai
homeassistant/components/weather/* @fabaff
homeassistant/components/webostv/* @bendavid
homeassistant/components/websocket_api/* @home-assistant/core
homeassistant/components/wemo/* @sqldiablo
homeassistant/components/wiffi/* @mampfes
homeassistant/components/withings/* @vangorra
homeassistant/components/wled/* @frenck

View File

@ -117,7 +117,8 @@ class TotpAuthModule(MultiFactorAuthModule):
Mfa module should extend SetupFlow
"""
user = await self.hass.auth.async_get_user(user_id) # type: ignore
user = await self.hass.auth.async_get_user(user_id)
assert user is not None
return TotpSetupFlow(self, self.input_schema, user)
async def async_setup_user(self, user_id: str, setup_data: Any) -> str:

View File

@ -175,7 +175,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
"""Initialize the login flow."""
self._auth_provider = auth_provider
self._auth_module_id: Optional[str] = None
self._auth_manager = auth_provider.hass.auth # type: ignore
self._auth_manager = auth_provider.hass.auth
self.available_mfa_modules: Dict[str, str] = {}
self.created_at = dt_util.utcnow()
self.invalid_mfa_times = 0
@ -224,6 +224,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
errors = {}
assert self._auth_module_id is not None
auth_module = self._auth_manager.get_auth_mfa_module(self._auth_module_id)
if auth_module is None:
# Given an invalid input to async_step_select_mfa_module
@ -234,7 +235,9 @@ class LoginFlow(data_entry_flow.FlowHandler):
auth_module, "async_initialize_login_mfa_step"
):
try:
await auth_module.async_initialize_login_mfa_step(self.user.id)
await auth_module.async_initialize_login_mfa_step( # type: ignore
self.user.id
)
except HomeAssistantError:
_LOGGER.exception("Error initializing MFA step")
return self.async_abort(reason="unknown_error")

View File

@ -1,6 +1,7 @@
"""Provide methods to bootstrap a Home Assistant instance."""
import asyncio
import contextlib
from datetime import datetime
import logging
import logging.handlers
import os
@ -20,7 +21,12 @@ from homeassistant.const import (
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import DATA_SETUP, DATA_SETUP_STARTED, async_setup_component
from homeassistant.setup import (
DATA_SETUP,
DATA_SETUP_STARTED,
async_set_domains_to_be_loaded,
async_setup_component,
)
from homeassistant.util.logging import async_activate_log_queue_handler
from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache
@ -34,12 +40,18 @@ DATA_LOGGING = "logging"
LOG_SLOW_STARTUP_INTERVAL = 60
DEBUGGER_INTEGRATIONS = {"ptvsd"}
DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"}
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
LOGGING_INTEGRATIONS = {"logger", "system_log", "sentry"}
STAGE_1_INTEGRATIONS = {
LOGGING_INTEGRATIONS = {
# Set log levels
"logger",
# Error logging
"system_log",
"sentry",
# To record data
"recorder",
}
STAGE_1_INTEGRATIONS = {
# To make sure we forward data to other instances
"mqtt_eventstream",
# To provide account link implementations
@ -50,7 +62,6 @@ STAGE_1_INTEGRATIONS = {
# as possible so problem integrations can
# be removed
"frontend",
"config",
}
@ -125,8 +136,12 @@ async def async_setup_hass(
await hass.async_block_till_done()
safe_mode = True
old_config = hass.config
hass = core.HomeAssistant()
hass.config.config_dir = config_dir
hass.config.skip_pip = old_config.skip_pip
hass.config.internal_url = old_config.internal_url
hass.config.external_url = old_config.external_url
hass.config.config_dir = old_config.config_dir
if safe_mode:
_LOGGER.info("Starting in safe mode")
@ -327,76 +342,130 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
return domains
async def _async_log_pending_setups(
domains: Set[str], setup_started: Dict[str, datetime]
) -> None:
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL."""
while True:
await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL)
remaining = [domain for domain in domains if domain in setup_started]
if remaining:
_LOGGER.info(
"Waiting on integrations to complete setup: %s", ", ".join(remaining),
)
async def async_setup_multi_components(
hass: core.HomeAssistant,
domains: Set[str],
config: Dict[str, Any],
setup_started: Dict[str, datetime],
) -> None:
"""Set up multiple domains. Log on failure."""
futures = {
domain: hass.async_create_task(async_setup_component(hass, domain, config))
for domain in domains
}
log_task = asyncio.create_task(_async_log_pending_setups(domains, setup_started))
await asyncio.wait(futures.values())
log_task.cancel()
errors = [domain for domain in domains if futures[domain].exception()]
for domain in errors:
exception = futures[domain].exception()
assert exception is not None
_LOGGER.error(
"Error setting up integration %s - received exception",
domain,
exc_info=(type(exception), exception, exception.__traceback__),
)
async def _async_set_up_integrations(
hass: core.HomeAssistant, config: Dict[str, Any]
) -> None:
"""Set up all the integrations."""
setup_started = hass.data[DATA_SETUP_STARTED] = {}
domains_to_setup = _get_domains(hass, config)
async def async_setup_multi_components(domains: Set[str]) -> None:
"""Set up multiple domains. Log on failure."""
# Resolve all dependencies so we know all integrations
# that will have to be loaded and start rightaway
integration_cache: Dict[str, loader.Integration] = {}
to_resolve = domains_to_setup
while to_resolve:
old_to_resolve = to_resolve
to_resolve = set()
async def _async_log_pending_setups() -> None:
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL."""
while True:
await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL)
remaining = [domain for domain in domains if domain in setup_started]
if remaining:
_LOGGER.info(
"Waiting on integrations to complete setup: %s",
", ".join(remaining),
)
futures = {
domain: hass.async_create_task(async_setup_component(hass, domain, config))
for domain in domains
}
log_task = asyncio.create_task(_async_log_pending_setups())
await asyncio.wait(futures.values())
log_task.cancel()
errors = [domain for domain in domains if futures[domain].exception()]
for domain in errors:
exception = futures[domain].exception()
_LOGGER.error(
"Error setting up integration %s - received exception",
domain,
exc_info=(type(exception), exception, exception.__traceback__),
integrations_to_process = [
int_or_exc
for int_or_exc in await asyncio.gather(
*(
loader.async_get_integration(hass, domain)
for domain in old_to_resolve
),
return_exceptions=True,
)
if isinstance(int_or_exc, loader.Integration)
]
resolve_dependencies_tasks = [
itg.resolve_dependencies()
for itg in integrations_to_process
if not itg.all_dependencies_resolved
]
domains = _get_domains(hass, config)
if resolve_dependencies_tasks:
await asyncio.gather(*resolve_dependencies_tasks)
for itg in integrations_to_process:
integration_cache[itg.domain] = itg
for dep in itg.all_dependencies:
if dep in domains_to_setup:
continue
domains_to_setup.add(dep)
to_resolve.add(dep)
_LOGGER.info("Domains to be set up: %s", domains_to_setup)
logging_domains = domains_to_setup & LOGGING_INTEGRATIONS
# Load logging as soon as possible
if logging_domains:
_LOGGER.info("Setting up logging: %s", logging_domains)
await async_setup_multi_components(hass, logging_domains, config, setup_started)
# Start up debuggers. Start these first in case they want to wait.
debuggers = domains & DEBUGGER_INTEGRATIONS
debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS
if debuggers:
_LOGGER.debug("Starting up debuggers %s", debuggers)
await async_setup_multi_components(debuggers)
domains -= DEBUGGER_INTEGRATIONS
_LOGGER.debug("Setting up debuggers: %s", debuggers)
await async_setup_multi_components(hass, debuggers, config, setup_started)
# Resolve all dependencies of all components so we can find the logging
# and integrations that need faster initialization.
resolved_domains_task = asyncio.gather(
*(loader.async_component_dependencies(hass, domain) for domain in domains),
return_exceptions=True,
)
# calculate what components to setup in what stage
stage_1_domains = set()
# Finish resolving domains
for dep_domains in await resolved_domains_task:
# Result is either a set or an exception. We ignore exceptions
# It will be properly handled during setup of the domain.
if isinstance(dep_domains, set):
domains.update(dep_domains)
# Find all dependencies of any dependency of any stage 1 integration that
# we plan on loading and promote them to stage 1
deps_promotion = STAGE_1_INTEGRATIONS
while deps_promotion:
old_deps_promotion = deps_promotion
deps_promotion = set()
# setup components
logging_domains = domains & LOGGING_INTEGRATIONS
stage_1_domains = domains & STAGE_1_INTEGRATIONS
stage_2_domains = domains - logging_domains - stage_1_domains
for domain in old_deps_promotion:
if domain not in domains_to_setup or domain in stage_1_domains:
continue
if logging_domains:
_LOGGER.info("Setting up %s", logging_domains)
stage_1_domains.add(domain)
await async_setup_multi_components(logging_domains)
dep_itg = integration_cache.get(domain)
if dep_itg is None:
continue
deps_promotion.update(dep_itg.all_dependencies)
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
# Kick off loading the registries. They don't need to be awaited.
asyncio.gather(
@ -405,49 +474,17 @@ async def _async_set_up_integrations(
hass.helpers.area_registry.async_get_registry(),
)
# Start setup
if stage_1_domains:
_LOGGER.info("Setting up %s", stage_1_domains)
_LOGGER.info("Setting up stage 1: %s", stage_1_domains)
await async_setup_multi_components(hass, stage_1_domains, config, setup_started)
await async_setup_multi_components(stage_1_domains)
# Enables after dependencies
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
# Load all integrations
after_dependencies: Dict[str, Set[str]] = {}
for int_or_exc in await asyncio.gather(
*(loader.async_get_integration(hass, domain) for domain in stage_2_domains),
return_exceptions=True,
):
# Exceptions are handled in async_setup_component.
if isinstance(int_or_exc, loader.Integration) and int_or_exc.after_dependencies:
after_dependencies[int_or_exc.domain] = set(int_or_exc.after_dependencies)
last_load = None
while stage_2_domains:
domains_to_load = set()
for domain in stage_2_domains:
after_deps = after_dependencies.get(domain)
# Load if integration has no after_dependencies or they are
# all loaded
if not after_deps or not after_deps - hass.config.components:
domains_to_load.add(domain)
if not domains_to_load or domains_to_load == last_load:
break
_LOGGER.debug("Setting up %s", domains_to_load)
await async_setup_multi_components(domains_to_load)
last_load = domains_to_load
stage_2_domains -= domains_to_load
# These are stage 2 domains that never have their after_dependencies
# satisfied.
if stage_2_domains:
_LOGGER.debug("Final set up: %s", stage_2_domains)
await async_setup_multi_components(stage_2_domains)
_LOGGER.info("Setting up stage 2: %s", stage_2_domains)
await async_setup_multi_components(hass, stage_2_domains, config, setup_started)
# Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up")

View File

@ -4,5 +4,8 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/abode",
"requirements": ["abodepy==0.19.0"],
"codeowners": ["@shred86"]
"codeowners": ["@shred86"],
"homekit": {
"models": ["Abode", "Iota"]
}
}

View File

@ -1,3 +0,0 @@
{
"title": "Abode"
}

View File

@ -71,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
except AdGuardHomeConnectionError as exception:
raise ConfigEntryNotReady from exception
if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
if version and LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
_LOGGER.error(
"This integration requires AdGuard Home v0.99.0 or higher to work correctly"
)

View File

@ -84,7 +84,7 @@ class AdGuardHomeFlowHandler(ConfigFlow):
errors["base"] = "connection_error"
return await self._show_setup_form(errors)
if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
if version and LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version):
return self.async_abort(
reason="adguard_home_outdated",
description_placeholders={
@ -105,7 +105,7 @@ class AdGuardHomeFlowHandler(ConfigFlow):
},
)
async def async_step_hassio(self, user_input=None):
async def async_step_hassio(self, discovery_info):
"""Prepare configuration for a Hass.io AdGuard Home add-on.
This flow is triggered by the discovery component.
@ -113,14 +113,14 @@ class AdGuardHomeFlowHandler(ConfigFlow):
entries = self._async_current_entries()
if not entries:
self._hassio_discovery = user_input
self._hassio_discovery = discovery_info
return await self.async_step_hassio_confirm()
cur_entry = entries[0]
if (
cur_entry.data[CONF_HOST] == user_input[CONF_HOST]
and cur_entry.data[CONF_PORT] == user_input[CONF_PORT]
cur_entry.data[CONF_HOST] == discovery_info[CONF_HOST]
and cur_entry.data[CONF_PORT] == discovery_info[CONF_PORT]
):
return self.async_abort(reason="single_instance_allowed")
@ -133,8 +133,8 @@ class AdGuardHomeFlowHandler(ConfigFlow):
cur_entry,
data={
**cur_entry.data,
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
CONF_HOST: discovery_info[CONF_HOST],
CONF_PORT: discovery_info[CONF_PORT],
},
)

View File

@ -23,13 +23,13 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Initialize the Agent config flow."""
self.device_config = {}
async def async_step_user(self, info=None):
async def async_step_user(self, user_input=None):
"""Handle an Agent config flow."""
errors = {}
if info is not None:
host = info[CONF_HOST]
port = info[CONF_PORT]
if user_input is not None:
host = user_input[CONF_HOST]
port = user_input[CONF_PORT]
server_origin = generate_url(host, port)
agent_client = Agent(server_origin, async_get_clientsession(self.hass))
@ -48,8 +48,8 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured(
updates={
CONF_HOST: info[CONF_HOST],
CONF_PORT: info[CONF_PORT],
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
SERVER_URL: server_origin,
}
)

View File

@ -4,7 +4,7 @@
"already_configured": "El dispositivo ya est\u00e1 configurado"
},
"error": {
"already_in_progress": "La configuraci\u00f3n del flujo para el dispositivo ya est\u00e1 en marcha.",
"already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en marcha.",
"device_unavailable": "El dispositivo no est\u00e1 disponible"
},
"step": {

View File

@ -10,7 +10,7 @@
"step": {
"user": {
"data": {
"api_key": "Airly API-n\u00f8kkel",
"api_key": "API-n\u00f8kkel",
"latitude": "Breddegrad",
"longitude": "Lengdegrad",
"name": "Navn p\u00e5 integrasjonen"

View File

@ -21,7 +21,7 @@
"node_pro": {
"data": {
"ip_address": "Enhetens IP-adresse / vertsnavn",
"password": "Passord for enhet"
"password": "Passord"
},
"description": "Overv\u00e5ke en personlig AirVisual-enhet. Passordet kan hentes fra enhetens brukergrensesnitt.",
"title": "Konfigurer en AirVisual Node / Pro"

View File

@ -1,12 +0,0 @@
{
"config": {
"step": {
"user": {
"data": {
"latitude": "Zemepisn\u00e1 \u0161\u00edrka",
"longitude": "Zemepisn\u00e1 d\u013a\u017eka"
}
}
}
}
}

View File

@ -2,7 +2,7 @@
from datetime import timedelta
import logging
from alarmdecoder import AlarmDecoder
from adext import AdExt
from alarmdecoder.devices import SerialDevice, SocketDevice, USBDevice
from alarmdecoder.util import NoDeviceError
import voluptuous as vol
@ -189,13 +189,13 @@ def setup(hass, config):
if device_type == "socket":
host = device[CONF_HOST]
port = device[CONF_DEVICE_PORT]
controller = AlarmDecoder(SocketDevice(interface=(host, port)))
controller = AdExt(SocketDevice(interface=(host, port)))
elif device_type == "serial":
path = device[CONF_DEVICE_PATH]
baud = device[CONF_DEVICE_BAUD]
controller = AlarmDecoder(SerialDevice(interface=path))
controller = AdExt(SerialDevice(interface=path))
elif device_type == "usb":
AlarmDecoder(USBDevice.find())
AdExt(USBDevice.find())
return False
controller.on_message += handle_message

View File

@ -16,6 +16,7 @@ from homeassistant.const import (
ATTR_CODE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
@ -108,6 +109,8 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
self._state = STATE_ALARM_TRIGGERED
elif message.armed_away:
self._state = STATE_ALARM_ARMED_AWAY
elif message.armed_home and (message.entry_delay_off or message.perimeter_only):
self._state = STATE_ALARM_ARMED_NIGHT
elif message.armed_home:
self._state = STATE_ALARM_ARMED_HOME
else:
@ -178,28 +181,27 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity):
def alarm_arm_away(self, code=None):
"""Send arm away command."""
if code:
if self._auto_bypass:
self.hass.data[DATA_AD].send(f"{code!s}6#")
self.hass.data[DATA_AD].send(f"{code!s}2")
elif not self._code_arm_required:
self.hass.data[DATA_AD].send("#2")
self.hass.data[DATA_AD].arm_away(
code=code,
code_arm_required=self._code_arm_required,
auto_bypass=self._auto_bypass,
)
def alarm_arm_home(self, code=None):
"""Send arm home command."""
if code:
if self._auto_bypass:
self.hass.data[DATA_AD].send(f"{code!s}6#")
self.hass.data[DATA_AD].send(f"{code!s}3")
elif not self._code_arm_required:
self.hass.data[DATA_AD].send("#3")
self.hass.data[DATA_AD].arm_home(
code=code,
code_arm_required=self._code_arm_required,
auto_bypass=self._auto_bypass,
)
def alarm_arm_night(self, code=None):
"""Send arm night command."""
if code:
self.hass.data[DATA_AD].send(f"{code!s}7")
elif not self._code_arm_required:
self.hass.data[DATA_AD].send("#7")
self.hass.data[DATA_AD].arm_night(
code=code,
code_arm_required=self._code_arm_required,
auto_bypass=self._auto_bypass,
)
def alarm_toggle_chime(self, code=None):
"""Send toggle chime command."""

View File

@ -2,6 +2,6 @@
"domain": "alarmdecoder",
"name": "AlarmDecoder",
"documentation": "https://www.home-assistant.io/integrations/alarmdecoder",
"requirements": ["alarmdecoder==1.13.2"],
"requirements": ["adext==0.3"],
"codeowners": ["@ajschmidt8"]
}

View File

@ -222,11 +222,6 @@ class Alert(ToggleEntity):
return STATE_ON
return STATE_IDLE
@property
def hidden(self):
"""Hide the alert when it is not firing."""
return not self._can_ack or not self._firing
async def watched_entity_change(self, entity, from_state, to_state):
"""Determine if the alert should start or stop."""
_LOGGER.debug("Watched entity (%s) has changed", entity)
@ -310,7 +305,9 @@ class Alert(ToggleEntity):
_LOGGER.debug(msg_payload)
for target in self._notifiers:
await self.hass.services.async_call(DOMAIN_NOTIFY, target, msg_payload)
await self.hass.services.async_call(
DOMAIN_NOTIFY, target, msg_payload, context=self._context
)
async def async_turn_on(self, **kwargs):
"""Async Unacknowledge alert."""

View File

@ -4,7 +4,6 @@ import logging
import voluptuous as vol
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_NAME
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv, entityfilter
from . import flash_briefings, intent, smart_home_http
@ -17,12 +16,12 @@ from .const import (
CONF_ENTITY_CONFIG,
CONF_FILTER,
CONF_LOCALE,
CONF_PASSWORD,
CONF_SUPPORTED_LOCALES,
CONF_TEXT,
CONF_TITLE,
CONF_UID,
DOMAIN,
EVENT_ALEXA_SMART_HOME,
)
_LOGGER = logging.getLogger(__name__)
@ -56,6 +55,7 @@ CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: {
CONF_FLASH_BRIEFINGS: {
vol.Required(CONF_PASSWORD): cv.string,
cv.string: vol.All(
cv.ensure_list,
[
@ -67,7 +67,7 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_DISPLAY_URL): cv.template,
}
],
)
),
},
# vol.Optional here would mean we couldn't distinguish between an empty
# smart_home: and none at all.
@ -80,28 +80,6 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass, config):
"""Activate the Alexa component."""
@callback
def async_describe_logbook_event(event):
"""Describe a logbook event."""
data = event.data
entity_id = data["request"].get("entity_id")
if entity_id:
state = hass.states.get(entity_id)
name = state.name if state else entity_id
message = f"send command {data['request']['namespace']}/{data['request']['name']} for {name}"
else:
message = (
f"send command {data['request']['namespace']}/{data['request']['name']}"
)
return {"name": "Amazon Alexa", "message": message, "entity_id": entity_id}
hass.components.logbook.async_describe_event(
DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event
)
if DOMAIN not in config:
return True

View File

@ -19,6 +19,7 @@ CONF_FILTER = "filter"
CONF_ENTITY_CONFIG = "entity_config"
CONF_ENDPOINT = "endpoint"
CONF_LOCALE = "locale"
CONF_PASSWORD = "password"
ATTR_UID = "uid"
ATTR_UPDATE_DATE = "updateDate"
@ -39,6 +40,7 @@ API_HEADER = "header"
API_PAYLOAD = "payload"
API_SCOPE = "scope"
API_CHANGE = "change"
API_PASSWORD = "password"
CONF_DESCRIPTION = "description"
CONF_DISPLAY_CATEGORIES = "display_categories"

View File

@ -1,15 +1,17 @@
"""Support for Alexa skill service end point."""
import copy
import hmac
import logging
import uuid
from homeassistant.components import http
from homeassistant.const import HTTP_NOT_FOUND
from homeassistant.const import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED
from homeassistant.core import callback
from homeassistant.helpers import template
import homeassistant.util.dt as dt_util
from .const import (
API_PASSWORD,
ATTR_MAIN_TEXT,
ATTR_REDIRECTION_URL,
ATTR_STREAM_URL,
@ -18,6 +20,7 @@ from .const import (
ATTR_UPDATE_DATE,
CONF_AUDIO,
CONF_DISPLAY_URL,
CONF_PASSWORD,
CONF_TEXT,
CONF_TITLE,
CONF_UID,
@ -39,6 +42,7 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
"""Handle Alexa Flash Briefing skill requests."""
url = FLASH_BRIEFINGS_API_ENDPOINT
requires_auth = False
name = "api:alexa:flash_briefings"
def __init__(self, hass, flash_briefings):
@ -52,7 +56,20 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
"""Handle Alexa Flash Briefing request."""
_LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id)
if self.flash_briefings.get(briefing_id) is None:
if request.query.get(API_PASSWORD) is None:
err = "No password provided for Alexa flash briefing: %s"
_LOGGER.error(err, briefing_id)
return b"", HTTP_UNAUTHORIZED
if not hmac.compare_digest(
request.query[API_PASSWORD].encode("utf-8"),
self.flash_briefings[CONF_PASSWORD].encode("utf-8"),
):
err = "Wrong password for Alexa flash briefing: %s"
_LOGGER.error(err, briefing_id)
return b"", HTTP_UNAUTHORIZED
if not isinstance(self.flash_briefings.get(briefing_id), list):
err = "No configured Alexa flash briefing was found for: %s"
_LOGGER.error(err, briefing_id)
return b"", HTTP_NOT_FOUND

View File

@ -0,0 +1,28 @@
"""Describe logbook events."""
from homeassistant.core import callback
from .const import DOMAIN, EVENT_ALEXA_SMART_HOME
@callback
def async_describe_events(hass, async_describe_event):
"""Describe logbook events."""
@callback
def async_describe_logbook_event(event):
"""Describe a logbook event."""
data = event.data
entity_id = data["request"].get("entity_id")
if entity_id:
state = hass.states.get(entity_id)
name = state.name if state else entity_id
message = f"send command {data['request']['namespace']}/{data['request']['name']} for {name}"
else:
message = (
f"send command {data['request']['namespace']}/{data['request']['name']}"
)
return {"name": "Amazon Alexa", "message": message, "entity_id": entity_id}
async_describe_event(DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event)

View File

@ -2,7 +2,14 @@
"domain": "alexa",
"name": "Amazon Alexa",
"documentation": "https://www.home-assistant.io/integrations/alexa",
"dependencies": ["http"],
"after_dependencies": ["logbook", "camera"],
"codeowners": ["@home-assistant/cloud", "@ochlocracy"]
"dependencies": [
"http"
],
"after_dependencies": [
"camera"
],
"codeowners": [
"@home-assistant/cloud",
"@ochlocracy"
]
}

View File

@ -94,12 +94,12 @@ class AlmondFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler):
data={"type": TYPE_LOCAL, "host": user_input["host"]},
)
async def async_step_hassio(self, user_input=None):
async def async_step_hassio(self, discovery_info):
"""Receive a Hass.io discovery."""
if self._async_current_entries():
return self.async_abort(reason="already_setup")
self.hassio_discovery = user_input
self.hassio_discovery = discovery_info
return await self.async_step_hassio_confirm()

View File

@ -1,3 +0,0 @@
{
"title": "Almond"
}

View File

@ -1,3 +0,0 @@
{
"title": "Ambiclimate"
}

View File

@ -10,6 +10,7 @@ from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_LOCATION,
ATTR_NAME,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
CONF_API_KEY,
DEGREE,
@ -126,6 +127,8 @@ TYPE_TEMPF = "tempf"
TYPE_TEMPINF = "tempinf"
TYPE_TOTALRAININ = "totalrainin"
TYPE_UV = "uv"
TYPE_PM25 = "pm25"
TYPE_PM25_24H = "pm25_24h"
TYPE_WEEKLYRAININ = "weeklyrainin"
TYPE_WINDDIR = "winddir"
TYPE_WINDDIR_AVG10M = "winddir_avg10m"
@ -218,6 +221,13 @@ SENSOR_TYPES = {
TYPE_TEMPINF: ("Inside Temp", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"),
TYPE_TOTALRAININ: ("Lifetime Rain", "in", TYPE_SENSOR, None),
TYPE_UV: ("uv", "Index", TYPE_SENSOR, None),
TYPE_PM25: ("PM25", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, TYPE_SENSOR, None),
TYPE_PM25_24H: (
"PM25 24h Avg",
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
TYPE_SENSOR,
None,
),
TYPE_WEEKLYRAININ: ("Weekly Rain", "in", TYPE_SENSOR, None),
TYPE_WINDDIR: ("Wind Dir", DEGREE, TYPE_SENSOR, None),
TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", DEGREE, TYPE_SENSOR, None),

View File

@ -1,3 +0,0 @@
{
"title": "Ambient PWS"
}

View File

@ -130,6 +130,10 @@ class CannotSnapshot(Exception):
"""Conditions are not valid for taking a snapshot."""
class AmcrestCommandFailed(Exception):
"""Amcrest camera command did not work."""
class AmcrestCam(Camera):
"""An implementation of an Amcrest IP camera."""
@ -367,12 +371,12 @@ class AmcrestCam(Camera):
self._model = resp.split("=")[-1]
else:
self._model = "unknown"
self.is_streaming = self._api.video_enabled
self._is_recording = self._api.record_mode == "Manual"
self._motion_detection_enabled = self._api.is_motion_detector_on()
self._audio_enabled = self._api.audio_enabled
self._motion_recording_enabled = self._api.is_record_on_motion_detection()
self._color_bw = _CBW[self._api.day_night_color]
self.is_streaming = self._get_video()
self._is_recording = self._get_recording()
self._motion_detection_enabled = self._get_motion_detection()
self._audio_enabled = self._get_audio()
self._motion_recording_enabled = self._get_motion_recording()
self._color_bw = self._get_color_mode()
self._rtsp_url = self._api.rtsp_url(typeno=self._resolution)
except AmcrestError as error:
log_update_error(_LOGGER, "get", self.name, "camera attributes", error)
@ -384,11 +388,11 @@ class AmcrestCam(Camera):
def turn_off(self):
"""Turn off camera."""
self._enable_video_stream(False)
self._enable_video(False)
def turn_on(self):
"""Turn on camera."""
self._enable_video_stream(True)
self._enable_video(True)
def enable_motion_detection(self):
"""Enable motion detection in the camera."""
@ -465,28 +469,53 @@ class AmcrestCam(Camera):
# Methods to send commands to Amcrest camera and handle errors
def _enable_video_stream(self, enable):
def _change_setting(self, value, attr, description, action="set"):
func = description.replace(" ", "_")
description = f"camera {description} to {value}"
tries = 3
while True:
try:
getattr(self, f"_set_{func}")(value)
new_value = getattr(self, f"_get_{func}")()
if new_value != value:
raise AmcrestCommandFailed
except (AmcrestError, AmcrestCommandFailed) as error:
if tries == 1:
log_update_error(_LOGGER, action, self.name, description, error)
return
log_update_error(
_LOGGER, action, self.name, description, error, logging.DEBUG
)
else:
if attr:
setattr(self, attr, new_value)
self.schedule_update_ha_state()
return
tries -= 1
def _get_video(self):
return self._api.video_enabled
def _set_video(self, enable):
self._api.video_enabled = enable
def _enable_video(self, enable):
"""Enable or disable camera video stream."""
# Given the way the camera's state is determined by
# is_streaming and is_recording, we can't leave
# recording on if video stream is being turned off.
if self.is_recording and not enable:
self._enable_recording(False)
try:
self._api.video_enabled = enable
except AmcrestError as error:
log_update_error(
_LOGGER,
"enable" if enable else "disable",
self.name,
"camera video stream",
error,
)
else:
self.is_streaming = enable
self.schedule_update_ha_state()
self._change_setting(enable, "is_streaming", "video")
if self._control_light:
self._enable_light(self._audio_enabled or self.is_streaming)
self._change_light()
def _get_recording(self):
return self._api.record_mode == "Manual"
def _set_recording(self, enable):
rec_mode = {"Automatic": 0, "Manual": 1}
self._api.record_mode = rec_mode["Manual" if enable else "Automatic"]
def _enable_recording(self, enable):
"""Turn recording on or off."""
@ -494,86 +523,56 @@ class AmcrestCam(Camera):
# is_streaming and is_recording, we can't leave
# video stream off if recording is being turned on.
if not self.is_streaming and enable:
self._enable_video_stream(True)
rec_mode = {"Automatic": 0, "Manual": 1}
try:
self._api.record_mode = rec_mode["Manual" if enable else "Automatic"]
except AmcrestError as error:
log_update_error(
_LOGGER,
"enable" if enable else "disable",
self.name,
"camera recording",
error,
)
else:
self._is_recording = enable
self.schedule_update_ha_state()
self._enable_video(True)
self._change_setting(enable, "_is_recording", "recording")
def _get_motion_detection(self):
return self._api.is_motion_detector_on()
def _set_motion_detection(self, enable):
self._api.motion_detection = str(enable).lower()
def _enable_motion_detection(self, enable):
"""Enable or disable motion detection."""
try:
self._api.motion_detection = str(enable).lower()
except AmcrestError as error:
log_update_error(
_LOGGER,
"enable" if enable else "disable",
self.name,
"camera motion detection",
error,
)
else:
self._motion_detection_enabled = enable
self.schedule_update_ha_state()
self._change_setting(enable, "_motion_detection_enabled", "motion detection")
def _get_audio(self):
return self._api.audio_enabled
def _set_audio(self, enable):
self._api.audio_enabled = enable
def _enable_audio(self, enable):
"""Enable or disable audio stream."""
try:
self._api.audio_enabled = enable
except AmcrestError as error:
log_update_error(
_LOGGER,
"enable" if enable else "disable",
self.name,
"camera audio stream",
error,
)
else:
self._audio_enabled = enable
self.schedule_update_ha_state()
self._change_setting(enable, "_audio_enabled", "audio")
if self._control_light:
self._enable_light(self._audio_enabled or self.is_streaming)
self._change_light()
def _enable_light(self, enable):
def _get_indicator_light(self):
return "true" in self._api.command(
"configManager.cgi?action=getConfig&name=LightGlobal"
).content.decode("utf-8")
def _set_indicator_light(self, enable):
self._api.command(
f"configManager.cgi?action=setConfig&LightGlobal[0].Enable={str(enable).lower()}"
)
def _change_light(self):
"""Enable or disable indicator light."""
try:
self._api.command(
f"configManager.cgi?action=setConfig&LightGlobal[0].Enable={str(enable).lower()}"
)
except AmcrestError as error:
log_update_error(
_LOGGER,
"enable" if enable else "disable",
self.name,
"indicator light",
error,
)
self._change_setting(
self._audio_enabled or self.is_streaming, None, "indicator light"
)
def _get_motion_recording(self):
return self._api.is_record_on_motion_detection()
def _set_motion_recording(self, enable):
self._api.motion_recording = str(enable).lower()
def _enable_motion_recording(self, enable):
"""Enable or disable motion recording."""
try:
self._api.motion_recording = str(enable).lower()
except AmcrestError as error:
log_update_error(
_LOGGER,
"enable" if enable else "disable",
self.name,
"camera motion recording",
error,
)
else:
self._motion_recording_enabled = enable
self.schedule_update_ha_state()
self._change_setting(enable, "_motion_recording_enabled", "motion recording")
def _goto_preset(self, preset):
"""Move camera position and zoom to preset."""
@ -584,17 +583,15 @@ class AmcrestCam(Camera):
_LOGGER, "move", self.name, f"camera to preset {preset}", error
)
def _get_color_mode(self):
return _CBW[self._api.day_night_color]
def _set_color_mode(self, cbw):
self._api.day_night_color = _CBW.index(cbw)
def _set_color_bw(self, cbw):
"""Set camera color mode."""
try:
self._api.day_night_color = _CBW.index(cbw)
except AmcrestError as error:
log_update_error(
_LOGGER, "set", self.name, f"camera color mode to {cbw}", error
)
else:
self._color_bw = cbw
self.schedule_update_ha_state()
self._change_setting(cbw, "_color_bw", "color mode")
def _start_tour(self, start):
"""Start camera tour."""

View File

@ -1,4 +1,6 @@
"""Helpers for amcrest component."""
import logging
from .const import DOMAIN
@ -7,9 +9,10 @@ def service_signal(service, *args):
return "_".join([DOMAIN, service, *args])
def log_update_error(logger, action, name, entity_type, error):
def log_update_error(logger, action, name, entity_type, error, level=logging.ERROR):
"""Log an update error."""
logger.error(
logger.log(
level,
"Could not %s %s %s due to error: %s",
action,
name,

View File

@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/androidtv",
"requirements": [
"adb-shell==0.1.3",
"androidtv==0.0.41",
"androidtv==0.0.43",
"pure-python-adb==0.2.2.dev0"
],
"codeowners": ["@JeffLIrion"]

View File

@ -5,27 +5,15 @@ import logging
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_ZONE,
EVENT_HOMEASSISTANT_STOP,
SERVICE_TURN_ON,
)
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .const import (
DEFAULT_NAME,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
DOMAIN_DATA_CONFIG,
DOMAIN_DATA_ENTRIES,
DOMAIN_DATA_TASKS,
SIGNAL_CLIENT_DATA,
@ -35,44 +23,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
def _optional_zone(value):
if value:
return ZONE_SCHEMA(value)
return ZONE_SCHEMA({})
def _zone_name_validator(config):
for zone, zone_config in config[CONF_ZONE].items():
if CONF_NAME not in zone_config:
zone_config[
CONF_NAME
] = f"{DEFAULT_NAME} ({config[CONF_HOST]}:{config[CONF_PORT]}) - {zone}"
return config
ZONE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(SERVICE_TURN_ON): cv.SERVICE_SCHEMA,
}
)
DEVICE_SCHEMA = vol.Schema(
vol.All(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.positive_int,
vol.Optional(CONF_ZONE, default={1: _optional_zone(None)}): {
vol.In([1, 2]): _optional_zone
},
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): cv.positive_int,
},
_zone_name_validator,
)
)
CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.115")
async def _await_cancel(task):
@ -83,27 +34,10 @@ async def _await_cancel(task):
pass
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA])}, extra=vol.ALLOW_EXTRA
)
async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up the component."""
hass.data[DOMAIN_DATA_ENTRIES] = {}
hass.data[DOMAIN_DATA_TASKS] = {}
hass.data[DOMAIN_DATA_CONFIG] = {}
for device in config[DOMAIN]:
hass.data[DOMAIN_DATA_CONFIG][(device[CONF_HOST], device[CONF_PORT])] = device
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={CONF_HOST: device[CONF_HOST], CONF_PORT: device[CONF_PORT]},
)
)
async def _stop(_):
asyncio.gather(
@ -116,21 +50,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
async def async_setup_entry(hass: HomeAssistantType, entry: config_entries.ConfigEntry):
"""Set up an access point from a config entry."""
"""Set up config entry."""
entries = hass.data[DOMAIN_DATA_ENTRIES]
tasks = hass.data[DOMAIN_DATA_TASKS]
client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
config = hass.data[DOMAIN_DATA_CONFIG].get(
(entry.data[CONF_HOST], entry.data[CONF_PORT]),
DEVICE_SCHEMA(
{CONF_HOST: entry.data[CONF_HOST], CONF_PORT: entry.data[CONF_PORT]}
),
)
tasks = hass.data.setdefault(DOMAIN_DATA_TASKS, {})
hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id] = {
"client": client,
"config": config,
}
entries[entry.entry_id] = client
task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL))
tasks[entry.entry_id] = task

View File

@ -1,27 +1,102 @@
"""Config flow to configure the Arcam FMJ component."""
from operator import itemgetter
import logging
from urllib.parse import urlparse
from arcam.fmj.client import Client, ConnectionFailed
from arcam.fmj.utils import get_uniqueid_from_host, get_uniqueid_from_udn
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_UDN
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN, DOMAIN_DATA_ENTRIES
_GETKEY = itemgetter(CONF_HOST, CONF_PORT)
_LOGGER = logging.getLogger(__name__)
def get_entry_client(hass, entry):
"""Retrieve client associated with a config entry."""
return hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id]
@config_entries.HANDLERS.register(DOMAIN)
class ArcamFmjFlowHandler(config_entries.ConfigFlow):
"""Handle a SimpliSafe config flow."""
"""Handle config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
entries = self.hass.config_entries.async_entries(DOMAIN)
import_key = _GETKEY(import_config)
for entry in entries:
if _GETKEY(entry.data) == import_key:
return self.async_abort(reason="already_setup")
async def _async_set_unique_id_and_update(self, host, port, uuid):
await self.async_set_unique_id(uuid)
self._abort_if_unique_id_configured({CONF_HOST: host, CONF_PORT: port})
return self.async_create_entry(title="Arcam FMJ", data=import_config)
async def _async_check_and_create(self, host, port):
client = Client(host, port)
try:
await client.start()
except ConnectionFailed:
return self.async_abort(reason="unable_to_connect")
finally:
await client.stop()
return self.async_create_entry(
title=f"{DEFAULT_NAME} ({host})", data={CONF_HOST: host, CONF_PORT: port},
)
async def async_step_user(self, user_input=None):
"""Handle a discovered device."""
errors = {}
if user_input is not None:
uuid = await get_uniqueid_from_host(
async_get_clientsession(self.hass), user_input[CONF_HOST]
)
if uuid:
await self._async_set_unique_id_and_update(
user_input[CONF_HOST], user_input[CONF_PORT], uuid
)
return await self._async_check_and_create(
user_input[CONF_HOST], user_input[CONF_PORT]
)
fields = {
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
return self.async_show_form(
step_id="user", data_schema=vol.Schema(fields), errors=errors
)
async def async_step_confirm(self, user_input=None):
"""Handle user-confirmation of discovered node."""
context = self.context # pylint: disable=no-member
placeholders = {
"host": context[CONF_HOST],
}
context["title_placeholders"] = placeholders
if user_input is not None:
return await self._async_check_and_create(
context[CONF_HOST], context[CONF_PORT]
)
return self.async_show_form(
step_id="confirm", description_placeholders=placeholders
)
async def async_step_ssdp(self, discovery_info):
"""Handle a discovered device."""
host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
port = DEFAULT_PORT
uuid = get_uniqueid_from_udn(discovery_info[ATTR_UPNP_UDN])
await self._async_set_unique_id_and_update(host, port, uuid)
context = self.context # pylint: disable=no-member
context[CONF_HOST] = host
context[CONF_PORT] = DEFAULT_PORT
return await self.async_step_confirm()

View File

@ -13,4 +13,3 @@ DEFAULT_SCAN_INTERVAL = 5
DOMAIN_DATA_ENTRIES = f"{DOMAIN}.entries"
DOMAIN_DATA_TASKS = f"{DOMAIN}.tasks"
DOMAIN_DATA_CONFIG = f"{DOMAIN}.config"

View File

@ -1,8 +1,14 @@
{
"domain": "arcam_fmj",
"name": "Arcam FMJ Receivers",
"config_flow": false,
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
"requirements": ["arcam-fmj==0.4.6"],
"requirements": ["arcam-fmj==0.5.1"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
"manufacturer": "ARCAM"
}
],
"codeowners": ["@elupus"]
}

View File

@ -1,6 +1,5 @@
"""Arcam media player."""
import logging
from typing import Optional
from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes
from arcam.fmj.state import State
@ -17,21 +16,13 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_NAME,
CONF_ZONE,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
from homeassistant.core import callback
from homeassistant.helpers.service import async_call_from_config
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.helpers.typing import HomeAssistantType
from .config_flow import get_entry_client
from .const import (
DOMAIN,
DOMAIN_DATA_ENTRIES,
EVENT_TURN_ON,
SIGNAL_CLIENT_DATA,
SIGNAL_CLIENT_STARTED,
@ -47,19 +38,17 @@ async def async_setup_entry(
async_add_entities,
):
"""Set up the configuration entry."""
data = hass.data[DOMAIN_DATA_ENTRIES][config_entry.entry_id]
client = data["client"]
config = data["config"]
client = get_entry_client(hass, config_entry)
async_add_entities(
[
ArcamFmj(
config_entry.title,
State(client, zone),
config_entry.unique_id or config_entry.entry_id,
zone_config[CONF_NAME],
zone_config.get(SERVICE_TURN_ON),
)
for zone, zone_config in config[CONF_ZONE].items()
for zone in [1, 2]
],
True,
)
@ -71,13 +60,13 @@ class ArcamFmj(MediaPlayerEntity):
"""Representation of a media device."""
def __init__(
self, state: State, uuid: str, name: str, turn_on: Optional[ConfigType]
self, device_name, state: State, uuid: str,
):
"""Initialize device."""
self._state = state
self._device_name = device_name
self._name = f"{device_name} - Zone: {state.zn}"
self._uuid = uuid
self._name = name
self._turn_on = turn_on
self._support = (
SUPPORT_SELECT_SOURCE
| SUPPORT_VOLUME_SET
@ -102,6 +91,11 @@ class ArcamFmj(MediaPlayerEntity):
)
)
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return self._state.zn == 1
@property
def unique_id(self):
"""Return unique identifier if known."""
@ -111,8 +105,12 @@ class ArcamFmj(MediaPlayerEntity):
def device_info(self):
"""Return a device description for device registry."""
return {
"identifiers": {(DOMAIN, self._state.client.host, self._state.client.port)},
"model": "FMJ",
"name": self._device_name,
"identifiers": {
(DOMAIN, self._uuid),
(DOMAIN, self._state.client.host, self._state.client.port),
},
"model": "Arcam FMJ AVR",
"manufacturer": "Arcam",
}
@ -229,15 +227,6 @@ class ArcamFmj(MediaPlayerEntity):
if self._state.get_power() is not None:
_LOGGER.debug("Turning on device using connection")
await self._state.set_power(True)
elif self._turn_on:
_LOGGER.debug("Turning on device using service call")
await async_call_from_config(
self.hass,
self._turn_on,
variables=None,
blocking=True,
validate_config=False,
)
else:
_LOGGER.debug("Firing event to turn on device")
self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id})

View File

@ -1,7 +1,28 @@
{
"config": {
"abort": {
"already_configured": "Device was already setup.",
"already_in_progress": "Config flow for device is already in progress.",
"unable_to_connect": "Unable to connect to device."
},
"error": {},
"flow_title": "Arcam FMJ on {host}",
"step": {
"confirm": {
"description": "Do you want to add Arcam FMJ on `{host}` to Home Assistant?"
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"description": "Please enter the host name or IP address of device."
}
}
},
"device_automation": {
"trigger_type": {
"turn_on": "{entity_name} was requested to turn on"
}
}
}
}

View File

@ -1,3 +0,0 @@
{
"title": "Arcam FMJ"
}

View File

@ -1,3 +0,0 @@
{
"title": "Arcam FMJ"
}

View File

@ -1,3 +0,0 @@
{
"title": "Arcam FMJ"
}

View File

@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "El dispositivo ya est\u00e1 configurado.",
"already_in_progress": "La configuraci\u00f3n del flujo para el dispositivo ya est\u00e1 en marcha.",
"already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en marcha.",
"unable_to_connect": "No se puede conectar con el dispositivo."
},
"flow_title": "Arcam FMJ en {host}",

View File

@ -1,4 +1,24 @@
{
"config": {
"abort": {
"already_configured": "L'appareil \u00e9tait d\u00e9j\u00e0 configur\u00e9.",
"already_in_progress": "Le flux de configuration de l'appareil est d\u00e9j\u00e0 en cours.",
"unable_to_connect": "Impossible de se connecter au p\u00e9riph\u00e9rique."
},
"error": {
"one": "Vide",
"other": "Vide"
},
"step": {
"user": {
"data": {
"host": "H\u00f4te",
"port": "Port"
},
"description": "Veuillez saisir le nom d\u2019h\u00f4te ou l\u2019adresse IP du p\u00e9riph\u00e9rique."
}
}
},
"device_automation": {
"trigger_type": {
"turn_on": "Il a \u00e9t\u00e9 demand\u00e9 \u00e0 {nom_de_l'entit\u00e9} de s'allumer"

View File

@ -1,3 +0,0 @@
{
"title": "Arcam FMJ"
}

View File

@ -1,4 +1,14 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "Nazwa hosta lub adres IP",
"port": "Port"
}
}
}
},
"device_automation": {
"trigger_type": {
"turn_on": "{entity_name} zostanie poproszony o w\u0142\u0105czenie"

View File

@ -1,3 +0,0 @@
{
"title": "Arcam FMJ"
}

View File

@ -1,3 +0,0 @@
{
"title": "Arcam FMJ"
}

View File

@ -24,6 +24,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
self.last_results = {}
self.success_init = False
self.connection = api
self._connect_error = False
async def async_connect(self):
"""Initialize connection to the router."""
@ -49,4 +50,15 @@ class AsusWrtDeviceScanner(DeviceScanner):
"""
_LOGGER.debug("Checking Devices")
self.last_results = await self.connection.async_get_connected_devices()
try:
self.last_results = await self.connection.async_get_connected_devices()
if self._connect_error:
self._connect_error = False
_LOGGER.error("Reconnected to ASUS router for device update")
except OSError as err:
if not self._connect_error:
self._connect_error = True
_LOGGER.error(
"Error connecting to ASUS router for device update: %s", err
)

View File

@ -49,6 +49,7 @@ class AsuswrtSensor(Entity):
self._devices = None
self._rates = None
self._speed = None
self._connect_error = False
@property
def name(self):
@ -62,9 +63,23 @@ class AsuswrtSensor(Entity):
async def async_update(self):
"""Fetch status from asuswrt."""
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()
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.error(
"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):

View File

@ -1,7 +1,7 @@
{
"config": {
"abort": {
"already_configured": "Nur ein Atag-Ger\u00e4t kann mit Home Assistant verbunden werden."
"already_configured": "Dieses Ger\u00e4t wurde bereits zu HomeAssistant hinzugef\u00fcgt"
},
"error": {
"connection_error": "Verbindung fehlgeschlagen, versuchen Sie es erneut"

View File

@ -3,7 +3,6 @@
"name": "Auth",
"documentation": "https://www.home-assistant.io/integrations/auth",
"dependencies": ["http"],
"after_dependencies": ["onboarding"],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal"
}

View File

@ -222,19 +222,6 @@ async def async_setup(hass, config):
hass, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({})
)
@callback
def async_describe_logbook_event(event):
"""Describe a logbook event."""
return {
"name": event.data.get(ATTR_NAME),
"message": "has been triggered",
"entity_id": event.data.get(ATTR_ENTITY_ID),
}
hass.components.logbook.async_describe_event(
DOMAIN, EVENT_AUTOMATION_TRIGGERED, async_describe_logbook_event
)
return True

View File

@ -0,0 +1,23 @@
"""Describe logbook events."""
from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME
from homeassistant.core import callback
from . import DOMAIN, EVENT_AUTOMATION_TRIGGERED
@callback
def async_describe_events(hass, async_describe_event): # type: ignore
"""Describe logbook events."""
@callback
def async_describe_logbook_event(event): # type: ignore
"""Describe a logbook event."""
return {
"name": event.data.get(ATTR_NAME),
"message": "has been triggered",
"entity_id": event.data.get(ATTR_ENTITY_ID),
}
async_describe_event(
DOMAIN, EVENT_AUTOMATION_TRIGGERED, async_describe_logbook_event
)

View File

@ -2,7 +2,12 @@
"domain": "automation",
"name": "Automation",
"documentation": "https://www.home-assistant.io/integrations/automation",
"after_dependencies": ["device_automation", "logbook", "webhook"],
"codeowners": ["@home-assistant/core"],
"after_dependencies": [
"device_automation",
"webhook"
],
"codeowners": [
"@home-assistant/core"
],
"quality_scale": "internal"
}

View File

@ -19,7 +19,7 @@ DEFAULT_QOS = 0
TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): mqtt.DOMAIN,
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Required(CONF_TOPIC): mqtt.util.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All(

View File

@ -0,0 +1,24 @@
{
"config": {
"abort": {
"already_configured": "This address is already configured."
},
"error": {
"invalid_country_code": "Unknown 2 letter country code.",
"invalid_house_number": "Invalid house number."
},
"step": {
"user": {
"data": {
"country_code": "2 Letter country code",
"house_number": "House number",
"house_number_extension": "House number extension",
"zip_code": "Zip code"
},
"description": "Enter your address",
"title": "Avri"
}
}
},
"title": "Avri"
}

View File

@ -0,0 +1,24 @@
{
"config": {
"abort": {
"already_configured": "Dit adres is reeds geconfigureerd."
},
"error": {
"invalid_country_code": "Onbekende landcode",
"invalid_house_number": "Ongeldig huisnummer."
},
"step": {
"user": {
"data": {
"country_code": "2 Letter landcode",
"house_number": "Huisnummer",
"house_number_extension": "Huisnummer toevoeging",
"zip_code": "Postcode"
},
"description": "Vul je adres in.",
"title": "Avri"
}
}
},
"title": "Avri"
}

View File

@ -1 +1,63 @@
"""The avri component."""
import asyncio
from datetime import timedelta
import logging
from avri.api import Avri
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import (
CONF_COUNTRY_CODE,
CONF_HOUSE_NUMBER,
CONF_HOUSE_NUMBER_EXTENSION,
CONF_ZIP_CODE,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor"]
SCAN_INTERVAL = timedelta(hours=4)
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Avri component."""
hass.data[DOMAIN] = {}
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Avri from a config entry."""
client = Avri(
postal_code=entry.data[CONF_ZIP_CODE],
house_nr=entry.data[CONF_HOUSE_NUMBER],
house_nr_extension=entry.data.get(CONF_HOUSE_NUMBER_EXTENSION),
country_code=entry.data[CONF_COUNTRY_CODE],
)
hass.data[DOMAIN][entry.entry_id] = client
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

View File

@ -0,0 +1,74 @@
"""Config flow for Avri component."""
import pycountry
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_ID
from .const import (
CONF_COUNTRY_CODE,
CONF_HOUSE_NUMBER,
CONF_HOUSE_NUMBER_EXTENSION,
CONF_ZIP_CODE,
DEFAULT_COUNTRY_CODE,
)
from .const import DOMAIN # pylint:disable=unused-import
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_ZIP_CODE): str,
vol.Required(CONF_HOUSE_NUMBER): int,
vol.Optional(CONF_HOUSE_NUMBER_EXTENSION): str,
vol.Optional(CONF_COUNTRY_CODE, default=DEFAULT_COUNTRY_CODE): str,
}
)
class AvriConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Avri config flow."""
VERSION = 1
async def _show_setup_form(self, errors=None):
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors or {},
)
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if user_input is None:
return await self._show_setup_form()
zip_code = user_input[CONF_ZIP_CODE].replace(" ", "").upper()
errors = {}
if user_input[CONF_HOUSE_NUMBER] <= 0:
errors[CONF_HOUSE_NUMBER] = "invalid_house_number"
return await self._show_setup_form(errors)
if not pycountry.countries.get(alpha_2=user_input[CONF_COUNTRY_CODE]):
errors[CONF_COUNTRY_CODE] = "invalid_country_code"
return await self._show_setup_form(errors)
unique_id = (
f"{zip_code}"
f" "
f"{user_input[CONF_HOUSE_NUMBER]}"
f'{user_input.get(CONF_HOUSE_NUMBER_EXTENSION, "")}'
)
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=unique_id,
data={
CONF_ID: unique_id,
CONF_ZIP_CODE: zip_code,
CONF_HOUSE_NUMBER: user_input[CONF_HOUSE_NUMBER],
CONF_HOUSE_NUMBER_EXTENSION: user_input.get(
CONF_HOUSE_NUMBER_EXTENSION, ""
),
CONF_COUNTRY_CODE: user_input[CONF_COUNTRY_CODE],
},
)

View File

@ -0,0 +1,8 @@
"""Constants for the Avri integration."""
CONF_COUNTRY_CODE = "country_code"
CONF_ZIP_CODE = "zip_code"
CONF_HOUSE_NUMBER = "house_number"
CONF_HOUSE_NUMBER_EXTENSION = "house_number_extension"
DOMAIN = "avri"
ICON = "mdi:trash-can-outline"
DEFAULT_COUNTRY_CODE = "NL"

View File

@ -2,6 +2,12 @@
"domain": "avri",
"name": "Avri",
"documentation": "https://www.home-assistant.io/integrations/avri",
"requirements": ["avri-api==0.1.7"],
"codeowners": ["@timvancann"]
}
"requirements": [
"avri-api==0.1.7",
"pycountry==19.8.18"
],
"codeowners": [
"@timvancann"
],
"config_flow": true
}

View File

@ -1,45 +1,25 @@
"""Support for Avri waste curbside collection pickup."""
from datetime import timedelta
import logging
from avri.api import Avri, AvriException
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, DEVICE_CLASS_TIMESTAMP
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from .const import DOMAIN, ICON
_LOGGER = logging.getLogger(__name__)
CONF_COUNTRY_CODE = "country_code"
CONF_ZIP_CODE = "zip_code"
CONF_HOUSE_NUMBER = "house_number"
CONF_HOUSE_NUMBER_EXTENSION = "house_number_extension"
DEFAULT_NAME = "avri"
ICON = "mdi:trash-can-outline"
SCAN_INTERVAL = timedelta(hours=4)
DEFAULT_COUNTRY_CODE = "NL"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ZIP_CODE): cv.string,
vol.Required(CONF_HOUSE_NUMBER): cv.positive_int,
vol.Optional(CONF_HOUSE_NUMBER_EXTENSION): cv.string,
vol.Optional(CONF_COUNTRY_CODE, default=DEFAULT_COUNTRY_CODE): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the Avri Waste platform."""
client = Avri(
postal_code=config[CONF_ZIP_CODE],
house_nr=config[CONF_HOUSE_NUMBER],
house_nr_extension=config.get(CONF_HOUSE_NUMBER_EXTENSION),
country_code=config[CONF_COUNTRY_CODE],
)
client = hass.data[DOMAIN][entry.entry_id]
integration_id = entry.data[CONF_ID]
try:
each_upcoming = client.upcoming_of_each()
@ -47,22 +27,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
raise PlatformNotReady from ex
else:
entities = [
AvriWasteUpcoming(config[CONF_NAME], client, upcoming.name)
AvriWasteUpcoming(client, upcoming.name, integration_id)
for upcoming in each_upcoming
]
add_entities(entities, True)
async_add_entities(entities, True)
class AvriWasteUpcoming(Entity):
"""Avri Waste Sensor."""
def __init__(self, name: str, client: Avri, waste_type: str):
def __init__(self, client: Avri, waste_type: str, integration_id: str):
"""Initialize the sensor."""
self._waste_type = waste_type
self._name = f"{name}_{self._waste_type}"
self._name = f"{self._waste_type}".title()
self._state = None
self._client = client
self._state_available = False
self._integration_id = integration_id
@property
def name(self):
@ -72,13 +53,7 @@ class AvriWasteUpcoming(Entity):
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return (
f"{self._waste_type}"
f"-{self._client.country_code}"
f"-{self._client.postal_code}"
f"-{self._client.house_nr}"
f"-{self._client.house_nr_extension}"
)
return (f"{self._integration_id}" f"-{self._waste_type}").replace(" ", "")
@property
def state(self):
@ -90,13 +65,21 @@ class AvriWasteUpcoming(Entity):
"""Return True if entity is available."""
return self._state_available
@property
def device_class(self):
"""Return the device class of the sensor."""
return DEVICE_CLASS_TIMESTAMP
@property
def icon(self):
"""Icon to use in the frontend."""
return ICON
def update(self):
"""Update device state."""
async def async_update(self):
"""Update the data."""
if not self.enabled:
return
try:
pickup_events = self._client.upcoming_of_each()
except AvriException as ex:

View File

@ -0,0 +1,24 @@
{
"title": "Avri",
"config": {
"abort": {
"already_configured": "This address is already configured."
},
"error": {
"invalid_house_number": "Invalid house number.",
"invalid_country_code": "Unknown 2 letter country code."
},
"step": {
"user": {
"data": {
"zip_code": "Zip code",
"house_number": "House number",
"house_number_extension": "House number extension",
"country_code": "2 Letter country code"
},
"description": "Enter your address",
"title": "Avri"
}
}
}
}

View File

@ -0,0 +1,7 @@
{
"config": {
"abort": {
"already_configured": "\u062a\u0645 \u062a\u0643\u0648\u064a\u0646 \u0647\u0630\u0627 \u0627\u0644\u0639\u0646\u0648\u0627\u0646 \u0628\u0627\u0644\u0641\u0639\u0644."
}
}
}

View File

@ -1,6 +1,10 @@
{
"config": {
"abort": {
"already_configured": "Cette adresse est d\u00e9j\u00e0 configur\u00e9e."
},
"error": {
"invalid_country_code": "Code pays \u00e0 2 lettres inconnu.",
"invalid_house_number": "Num\u00e9ro de maison invalide."
},
"step": {

View File

@ -1 +1,112 @@
"""The awair component."""
from asyncio import gather
from typing import Any, Optional
from async_timeout import timeout
from python_awair import Awair
from python_awair.exceptions import AuthError
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import API_TIMEOUT, DOMAIN, LOGGER, UPDATE_INTERVAL, AwairResult
PLATFORMS = ["sensor"]
async def async_setup(hass: HomeAssistant, config: Config) -> bool:
"""Set up Awair integration."""
return True
async def async_setup_entry(hass, config_entry) -> bool:
"""Set up Awair integration from a config entry."""
session = async_get_clientsession(hass)
coordinator = AwairDataUpdateCoordinator(hass, config_entry, session)
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = coordinator
for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
async def async_unload_entry(hass, config_entry) -> bool:
"""Unload Awair configuration."""
tasks = []
for platform in PLATFORMS:
tasks.append(
hass.config_entries.async_forward_entry_unload(config_entry, platform)
)
unload_ok = all(await gather(*tasks))
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
class AwairDataUpdateCoordinator(DataUpdateCoordinator):
"""Define a wrapper class to update Awair data."""
def __init__(self, hass, config_entry, session) -> None:
"""Set up the AwairDataUpdateCoordinator class."""
access_token = config_entry.data[CONF_ACCESS_TOKEN]
self._awair = Awair(access_token=access_token, session=session)
self._config_entry = config_entry
super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL)
async def _async_update_data(self) -> Optional[Any]:
"""Update data via Awair client library."""
with timeout(API_TIMEOUT):
try:
LOGGER.debug("Fetching users and devices")
user = await self._awair.user()
devices = await user.devices()
results = await gather(
*[self._fetch_air_data(device) for device in devices]
)
return {result.device.uuid: result for result in results}
except AuthError as err:
flow_context = {
"source": "reauth",
"unique_id": self._config_entry.unique_id,
}
matching_flows = [
flow
for flow in self.hass.config_entries.flow.async_progress()
if flow["context"] == flow_context
]
if not matching_flows:
self.hass.async_create_task(
self.hass.config_entries.flow.async_init(
DOMAIN, context=flow_context, data=self._config_entry.data,
)
)
raise UpdateFailed(err)
except Exception as err:
raise UpdateFailed(err)
async def _fetch_air_data(self, device):
"""Fetch latest air quality data."""
LOGGER.debug("Fetching data for %s", device.uuid)
air_data = await device.air_data_latest()
LOGGER.debug(air_data)
return AwairResult(device=device, air_data=air_data)

View File

@ -0,0 +1,109 @@
"""Config flow for Awair."""
from typing import Optional
from python_awair import Awair
from python_awair.exceptions import AuthError, AwairError
import voluptuous as vol
from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigFlow
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, LOGGER # pylint: disable=unused-import
class AwairFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for Awair."""
VERSION = 1
CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL
async def async_step_import(self, conf: dict):
"""Import a configuration from config.yaml."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason="already_setup")
user, error = await self._check_connection(conf[CONF_ACCESS_TOKEN])
if error is not None:
return self.async_abort(reason=error)
await self.async_set_unique_id(user.email)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"{user.email} ({user.user_id})",
data={CONF_ACCESS_TOKEN: conf[CONF_ACCESS_TOKEN]},
)
async def async_step_user(self, user_input: Optional[dict] = None):
"""Handle a flow initialized by the user."""
errors = {}
if user_input is not None:
user, error = await self._check_connection(user_input[CONF_ACCESS_TOKEN])
if user is not None:
await self.async_set_unique_id(user.email)
self._abort_if_unique_id_configured()
title = f"{user.email} ({user.user_id})"
return self.async_create_entry(title=title, data=user_input)
if error != "auth":
return self.async_abort(reason=error)
errors = {CONF_ACCESS_TOKEN: "auth"}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}),
errors=errors,
)
async def async_step_reauth(self, user_input: Optional[dict] = None):
"""Handle re-auth if token invalid."""
errors = {}
if user_input is not None:
access_token = user_input[CONF_ACCESS_TOKEN]
_, error = await self._check_connection(access_token)
if error is None:
for entry in self._async_current_entries():
if entry.unique_id == self.unique_id:
self.hass.config_entries.async_update_entry(
entry, data=user_input
)
return self.async_abort(reason="reauth_successful")
if error != "auth":
return self.async_abort(reason=error)
errors = {CONF_ACCESS_TOKEN: error}
return self.async_show_form(
step_id="reauth",
data_schema=vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}),
errors=errors,
)
async def _check_connection(self, access_token: str):
"""Check the access token is valid."""
session = async_get_clientsession(self.hass)
awair = Awair(access_token=access_token, session=session)
try:
user = await awair.user()
devices = await user.devices()
if not devices:
return (None, "no_devices")
return (user, None)
except AuthError:
return (None, "auth")
except AwairError as err:
LOGGER.error("Unexpected API error: %s", err)
return (None, "unknown")

View File

@ -0,0 +1,120 @@
"""Constants for the Awair component."""
from dataclasses import dataclass
from datetime import timedelta
import logging
from python_awair.devices import AwairDevice
from homeassistant.const import (
ATTR_DEVICE_CLASS,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
TEMP_CELSIUS,
UNIT_PERCENTAGE,
)
API_CO2 = "carbon_dioxide"
API_DUST = "dust"
API_HUMID = "humidity"
API_LUX = "illuminance"
API_PM10 = "particulate_matter_10"
API_PM25 = "particulate_matter_2_5"
API_SCORE = "score"
API_SPL_A = "sound_pressure_level"
API_TEMP = "temperature"
API_TIMEOUT = 20
API_VOC = "volatile_organic_compounds"
ATTRIBUTION = "Awair air quality sensor"
ATTR_ICON = "icon"
ATTR_LABEL = "label"
ATTR_UNIT = "unit"
ATTR_UNIQUE_ID = "unique_id"
DOMAIN = "awair"
DUST_ALIASES = [API_PM25, API_PM10]
LOGGER = logging.getLogger(__package__)
UPDATE_INTERVAL = timedelta(minutes=5)
SENSOR_TYPES = {
API_SCORE: {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:blur",
ATTR_UNIT: UNIT_PERCENTAGE,
ATTR_LABEL: "Awair score",
ATTR_UNIQUE_ID: "score", # matches legacy format
},
API_HUMID: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
ATTR_ICON: None,
ATTR_UNIT: UNIT_PERCENTAGE,
ATTR_LABEL: "Humidity",
ATTR_UNIQUE_ID: "HUMID", # matches legacy format
},
API_LUX: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE,
ATTR_ICON: None,
ATTR_UNIT: "lx",
ATTR_LABEL: "Illuminance",
ATTR_UNIQUE_ID: "illuminance",
},
API_SPL_A: {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:ear-hearing",
ATTR_UNIT: "dBa",
ATTR_LABEL: "Sound level",
ATTR_UNIQUE_ID: "sound_level",
},
API_VOC: {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:cloud",
ATTR_UNIT: CONCENTRATION_PARTS_PER_BILLION,
ATTR_LABEL: "Volatile organic compounds",
ATTR_UNIQUE_ID: "VOC", # matches legacy format
},
API_TEMP: {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_ICON: None,
ATTR_UNIT: TEMP_CELSIUS,
ATTR_LABEL: "Temperature",
ATTR_UNIQUE_ID: "TEMP", # matches legacy format
},
API_PM25: {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:blur",
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_LABEL: "PM2.5",
ATTR_UNIQUE_ID: "PM25", # matches legacy format
},
API_PM10: {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:blur",
ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_LABEL: "PM10",
ATTR_UNIQUE_ID: "PM10", # matches legacy format
},
API_CO2: {
ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:cloud",
ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION,
ATTR_LABEL: "Carbon dioxide",
ATTR_UNIQUE_ID: "CO2", # matches legacy format
},
}
@dataclass
class AwairResult:
"""Wrapper class to hold an awair device and set of air data."""
device: AwairDevice
air_data: dict

View File

@ -2,6 +2,7 @@
"domain": "awair",
"name": "Awair",
"documentation": "https://www.home-assistant.io/integrations/awair",
"requirements": ["python_awair==0.0.4"],
"codeowners": ["@danielsjf"]
"requirements": ["python_awair==0.1.1"],
"codeowners": ["@ahayworth", "@danielsjf"],
"config_flow": true
}

View File

@ -1,248 +1,245 @@
"""Support for the Awair indoor air quality monitor."""
"""Support for Awair sensors."""
from datetime import timedelta
import logging
import math
from typing import Callable, List, Optional
from python_awair import AwairClient
from python_awair.devices import AwairDevice
import voluptuous as vol
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONF_ACCESS_TOKEN,
CONF_DEVICES,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
TEMP_CELSIUS,
UNIT_PERCENTAGE,
)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.components.awair import AwairDataUpdateCoordinator, AwairResult
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_ACCESS_TOKEN
from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle, dt
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
_LOGGER = logging.getLogger(__name__)
from .const import (
API_DUST,
API_PM25,
API_SCORE,
API_TEMP,
API_VOC,
ATTR_ICON,
ATTR_LABEL,
ATTR_UNIQUE_ID,
ATTR_UNIT,
ATTRIBUTION,
DOMAIN,
DUST_ALIASES,
LOGGER,
SENSOR_TYPES,
)
ATTR_SCORE = "score"
ATTR_TIMESTAMP = "timestamp"
ATTR_LAST_API_UPDATE = "last_api_update"
ATTR_COMPONENT = "component"
ATTR_VALUE = "value"
ATTR_SENSORS = "sensors"
CONF_UUID = "uuid"
DEVICE_CLASS_PM2_5 = "PM2.5"
DEVICE_CLASS_PM10 = "PM10"
DEVICE_CLASS_CARBON_DIOXIDE = "CO2"
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS = "VOC"
DEVICE_CLASS_SCORE = "score"
SENSOR_TYPES = {
"TEMP": {
"device_class": DEVICE_CLASS_TEMPERATURE,
"unit_of_measurement": TEMP_CELSIUS,
"icon": "mdi:thermometer",
},
"HUMID": {
"device_class": DEVICE_CLASS_HUMIDITY,
"unit_of_measurement": UNIT_PERCENTAGE,
"icon": "mdi:water-percent",
},
"CO2": {
"device_class": DEVICE_CLASS_CARBON_DIOXIDE,
"unit_of_measurement": CONCENTRATION_PARTS_PER_MILLION,
"icon": "mdi:periodic-table-co2",
},
"VOC": {
"device_class": DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
"unit_of_measurement": CONCENTRATION_PARTS_PER_BILLION,
"icon": "mdi:cloud",
},
# Awair docs don't actually specify the size they measure for 'dust',
# but 2.5 allows the sensor to show up in HomeKit
"DUST": {
"device_class": DEVICE_CLASS_PM2_5,
"unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"icon": "mdi:cloud",
},
"PM25": {
"device_class": DEVICE_CLASS_PM2_5,
"unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"icon": "mdi:cloud",
},
"PM10": {
"device_class": DEVICE_CLASS_PM10,
"unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"icon": "mdi:cloud",
},
"score": {
"device_class": DEVICE_CLASS_SCORE,
"unit_of_measurement": UNIT_PERCENTAGE,
"icon": "mdi:percent",
},
}
AWAIR_QUOTA = 300
# This is the minimum time between throttled update calls.
# Don't bother asking us for state more often than that.
SCAN_INTERVAL = timedelta(minutes=5)
AWAIR_DEVICE_SCHEMA = vol.Schema({vol.Required(CONF_UUID): cv.string})
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [AWAIR_DEVICE_SCHEMA]),
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_ACCESS_TOKEN): cv.string}, extra=vol.ALLOW_EXTRA,
)
# Awair *heavily* throttles calls that get user information,
# and calls that get the list of user-owned devices - they
# allow 30 per DAY. So, we permit a user to provide a static
# list of devices, and they may provide the same set of information
# that the devices() call would return. However, the only thing
# used at this time is the `uuid` value.
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Connect to the Awair API and find devices."""
"""Import Awair configuration from YAML."""
LOGGER.warning(
"Loading Awair via platform setup is deprecated. Please remove it from your configuration."
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config,
)
)
token = config[CONF_ACCESS_TOKEN]
client = AwairClient(token, session=async_get_clientsession(hass))
try:
all_devices = []
devices = config.get(CONF_DEVICES, await client.devices())
async def async_setup_entry(
hass: HomeAssistantType,
config_entry: ConfigType,
async_add_entities: Callable[[List[Entity], bool], None],
):
"""Set up Awair sensor entity based on a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
sensors = []
# Try to throttle dynamically based on quota and number of devices.
throttle_minutes = math.ceil(60 / ((AWAIR_QUOTA / len(devices)) / 24))
throttle = timedelta(minutes=throttle_minutes)
data: List[AwairResult] = coordinator.data.values()
for result in data:
if result.air_data:
sensors.append(AwairSensor(API_SCORE, result.device, coordinator))
device_sensors = result.air_data.sensors.keys()
for sensor in device_sensors:
if sensor in SENSOR_TYPES:
sensors.append(AwairSensor(sensor, result.device, coordinator))
for device in devices:
_LOGGER.debug("Found awair device: %s", device)
awair_data = AwairData(client, device[CONF_UUID], throttle)
await awair_data.async_update()
for sensor in SENSOR_TYPES:
if sensor in awair_data.data:
awair_sensor = AwairSensor(awair_data, device, sensor, throttle)
all_devices.append(awair_sensor)
# The "DUST" sensor for Awair is a combo pm2.5/pm10 sensor only
# present on first-gen devices in lieu of separate pm2.5/pm10 sensors.
# We handle that by creating fake pm2.5/pm10 sensors that will always
# report identical values, and we let users decide how they want to use
# that data - because we can't really tell what kind of particles the
# "DUST" sensor actually detected. However, it's still useful data.
if API_DUST in device_sensors:
for alias_kind in DUST_ALIASES:
sensors.append(AwairSensor(alias_kind, result.device, coordinator))
async_add_entities(all_devices, True)
return
except AwairClient.AuthError:
_LOGGER.error("Awair API access_token invalid")
except AwairClient.RatelimitError:
_LOGGER.error("Awair API ratelimit exceeded.")
except (
AwairClient.QueryError,
AwairClient.NotFoundError,
AwairClient.GenericError,
) as error:
_LOGGER.error("Unexpected Awair API error: %s", error)
raise PlatformNotReady
async_add_entities(sensors)
class AwairSensor(Entity):
"""Implementation of an Awair device."""
"""Defines an Awair sensor entity."""
def __init__(self, data, device, sensor_type, throttle):
"""Initialize the sensor."""
self._uuid = device[CONF_UUID]
self._device_class = SENSOR_TYPES[sensor_type]["device_class"]
self._name = f"Awair {self._device_class}"
unit = SENSOR_TYPES[sensor_type]["unit_of_measurement"]
self._unit_of_measurement = unit
self._data = data
self._type = sensor_type
self._throttle = throttle
def __init__(
self, kind: str, device: AwairDevice, coordinator: AwairDataUpdateCoordinator,
) -> None:
"""Set up an individual AwairSensor."""
self._kind = kind
self._device = device
self._coordinator = coordinator
@property
def name(self):
def should_poll(self) -> bool:
"""Return the polling requirement of the entity."""
return False
@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
name = SENSOR_TYPES[self._kind][ATTR_LABEL]
if self._device.name:
name = f"{self._device.name} {name}"
return name
@property
def device_class(self):
"""Return the device class."""
return self._device_class
def unique_id(self) -> str:
"""Return the uuid as the unique_id."""
unique_id_tag = SENSOR_TYPES[self._kind][ATTR_UNIQUE_ID]
# This integration used to create a sensor that was labelled as a "PM2.5"
# sensor for first-gen Awair devices, but its unique_id reflected the truth:
# under the hood, it was a "DUST" sensor. So we preserve that specific unique_id
# for users with first-gen devices that are upgrading.
if self._kind == API_PM25 and API_DUST in self._air_data.sensors:
unique_id_tag = "DUST"
return f"{self._device.uuid}_{unique_id_tag}"
@property
def icon(self):
"""Icon to use in the frontend."""
return SENSOR_TYPES[self._type]["icon"]
def available(self) -> bool:
"""Determine if the sensor is available based on API results."""
# If the last update was successful...
if self._coordinator.last_update_success and self._air_data:
# and the results included our sensor type...
if self._kind in self._air_data.sensors:
# then we are available.
return True
# or, we're a dust alias
if self._kind in DUST_ALIASES and API_DUST in self._air_data.sensors:
return True
# or we are API_SCORE
if self._kind == API_SCORE:
# then we are available.
return True
# Otherwise, we are not.
return False
@property
def state(self):
"""Return the state of the device."""
return self._data.data[self._type]
def state(self) -> float:
"""Return the state, rounding off to reasonable values."""
state: float
# Special-case for "SCORE", which we treat as the AQI
if self._kind == API_SCORE:
state = self._air_data.score
elif self._kind in DUST_ALIASES and API_DUST in self._air_data.sensors:
state = self._air_data.sensors.dust
else:
state = self._air_data.sensors[self._kind]
if self._kind == API_VOC or self._kind == API_SCORE:
return round(state)
if self._kind == API_TEMP:
return round(state, 1)
return round(state, 2)
@property
def device_state_attributes(self):
"""Return additional attributes."""
return self._data.attrs
# The Awair device should be reporting metrics in quite regularly.
# Based on the raw data from the API, it looks like every ~10 seconds
# is normal. Here we assert that the device is not available if the
# last known API timestamp is more than (3 * throttle) minutes in the
# past. It implies that either hass is somehow unable to query the API
# for new data or that the device is not checking in. Either condition
# fits the definition for 'not available'. We pick (3 * throttle) minutes
# to allow for transient errors to correct themselves.
@property
def available(self):
"""Device availability based on the last update timestamp."""
if ATTR_LAST_API_UPDATE not in self.device_state_attributes:
return False
last_api_data = self.device_state_attributes[ATTR_LAST_API_UPDATE]
return (dt.utcnow() - last_api_data) < (3 * self._throttle)
def icon(self) -> str:
"""Return the icon."""
return SENSOR_TYPES[self._kind][ATTR_ICON]
@property
def unique_id(self):
"""Return the unique id of this entity."""
return f"{self._uuid}_{self._type}"
def device_class(self) -> str:
"""Return the device_class."""
return SENSOR_TYPES[self._kind][ATTR_DEVICE_CLASS]
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return self._unit_of_measurement
def unit_of_measurement(self) -> str:
"""Return the unit the value is expressed in."""
return SENSOR_TYPES[self._kind][ATTR_UNIT]
async def async_update(self):
"""Get the latest data."""
await self._data.async_update()
@property
def device_state_attributes(self) -> dict:
"""Return the Awair Index alongside state attributes.
The Awair Index is a subjective score ranging from 0-4 (inclusive) that
is is used by the Awair app when displaying the relative "safety" of a
given measurement. Each value is mapped to a color indicating the safety:
class AwairData:
"""Get data from Awair API."""
0: green
1: yellow
2: light-orange
3: orange
4: red
def __init__(self, client, uuid, throttle):
"""Initialize the data object."""
self._client = client
self._uuid = uuid
self.data = {}
self.attrs = {}
self.async_update = Throttle(throttle)(self._async_update)
The API indicates that both positive and negative values may be returned,
but the negative values are mapped to identical colors as the positive values.
Knowing that, we just return the absolute value of a given index so that
users don't have to handle positive/negative values that ultimately "mean"
the same thing.
async def _async_update(self):
"""Get the data from Awair API."""
resp = await self._client.air_data_latest(self._uuid)
https://docs.developer.getawair.com/?version=latest#awair-score-and-index
"""
attrs = {ATTR_ATTRIBUTION: ATTRIBUTION}
if self._kind in self._air_data.indices:
attrs["awair_index"] = abs(self._air_data.indices[self._kind])
elif self._kind in DUST_ALIASES and API_DUST in self._air_data.indices:
attrs["awair_index"] = abs(self._air_data.indices.dust)
if not resp:
return
return attrs
timestamp = dt.parse_datetime(resp[0][ATTR_TIMESTAMP])
self.attrs[ATTR_LAST_API_UPDATE] = timestamp
self.data[ATTR_SCORE] = resp[0][ATTR_SCORE]
@property
def device_info(self) -> dict:
"""Device information."""
info = {
"identifiers": {(DOMAIN, self._device.uuid)},
"manufacturer": "Awair",
"model": self._device.model,
}
# The air_data_latest call only returns one item, so this should
# be safe to only process one entry.
for sensor in resp[0][ATTR_SENSORS]:
self.data[sensor[ATTR_COMPONENT]] = round(sensor[ATTR_VALUE], 1)
if self._device.name:
info["name"] = self._device.name
_LOGGER.debug("Got Awair Data for %s: %s", self._uuid, self.data)
if self._device.mac_address:
info["connections"] = {
(dr.CONNECTION_NETWORK_MAC, self._device.mac_address)
}
return info
async def async_added_to_hass(self) -> None:
"""Connect to dispatcher listening for entity data notifications."""
self.async_on_remove(
self._coordinator.async_add_listener(self.async_write_ha_state)
)
async def async_update(self) -> None:
"""Update Awair entity."""
await self._coordinator.async_request_refresh()
@property
def _air_data(self) -> Optional[AwairResult]:
"""Return the latest data for our device, or None."""
result: Optional[AwairResult] = self._coordinator.data.get(self._device.uuid)
if result:
return result.air_data
return None

View File

@ -0,0 +1,29 @@
{
"config": {
"step": {
"user": {
"description": "You must register for an Awair developer access token at: https://developer.getawair.com/onboard/login",
"data": {
"access_token": "[%key:common::config_flow::data::access_token%]",
"email": "[%key:common::config_flow::data::email%]"
}
},
"reauth": {
"description": "Please re-enter your Awair developer access token.",
"data": {
"access_token": "[%key:common::config_flow::data::access_token%]",
"email": "[%key:common::config_flow::data::email%]"
}
}
},
"error": {
"auth": "[%key:common::config_flow::error::invalid_access_token%]",
"unknown": "Unknown Awair API error."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"no_devices": "[%key:common::config_flow::abort::no_devices_found%]",
"reauth_successful": "[%key:common::config_flow::data::access_token%] updated successfully"
}
}
}

View File

@ -0,0 +1,28 @@
{
"config": {
"abort": {
"already_configured": "El compte ja ha estat configurat",
"no_devices": "No s'han trobat dispositius a la xarxa",
"reauth_successful": "Token d'acc\u00e9s actualitzat correctament"
},
"error": {
"auth": "Token d'acc\u00e9s no v\u00e0lid",
"unknown": "Error desconegut de l'API Awair."
},
"step": {
"reauth": {
"data": {
"access_token": "Token d'acc\u00e9s",
"email": "Correu electr\u00f2nic"
},
"description": "Torna a introduir el token d'acc\u00e9s de desenvolupador d'Awair."
},
"user": {
"data": {
"access_token": "Token d'acc\u00e9s",
"email": "Correu electr\u00f2nic"
}
}
}
}
}

View File

@ -0,0 +1,29 @@
{
"config": {
"abort": {
"already_configured": "Account is already configured",
"no_devices": "No devices found on the network",
"reauth_successful": "Access Token updated successfully"
},
"error": {
"auth": "Invalid access token",
"unknown": "Unknown Awair API error."
},
"step": {
"reauth": {
"data": {
"access_token": "Access Token",
"email": "Email"
},
"description": "Please re-enter your Awair developer access token."
},
"user": {
"data": {
"access_token": "Access Token",
"email": "Email"
},
"description": "You must register for an Awair developer access token at: https://developer.getawair.com/onboard/login"
}
}
}
}

View File

@ -0,0 +1,29 @@
{
"config": {
"abort": {
"already_configured": "La cuenta ya ha sido configurada",
"no_devices": "No se encontraron dispositivos en la red",
"reauth_successful": "Token de acceso actualizado correctamente "
},
"error": {
"auth": "Token de acceso no v\u00e1lido",
"unknown": "Error desconocido en API Awair"
},
"step": {
"reauth": {
"data": {
"access_token": "Token de acceso",
"email": "Correo electr\u00f3nico"
},
"description": "Por favor, vuelve a introducir tu token de acceso de desarrollador Awair."
},
"user": {
"data": {
"access_token": "Token de acceso",
"email": "Correo electr\u00f3nico"
},
"description": "Debes registrarte para obtener un token de acceso de desarrollador Awair en: https://developer.getawair.com/onboard/login"
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"error": {
"unknown": "Ukjent Awair API-feil."
},
"step": {
"reauth": {
"data": {
"email": "Epost"
},
"description": "Skriv inn tilgangstokenet for Awair-utviklere p\u00e5 nytt."
},
"user": {
"data": {
"email": "Epost "
},
"description": "Du m\u00e5 registrere deg for et Awair-utviklertilgangstoken p\u00e5: https://developer.getawair.com/onboard/login"
}
}
}
}

View File

@ -0,0 +1,29 @@
{
"config": {
"abort": {
"already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.",
"no_devices": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.",
"reauth_successful": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d."
},
"error": {
"auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.",
"unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"step": {
"reauth": {
"data": {
"access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430",
"email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b"
},
"description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430."
},
"user": {
"data": {
"access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430",
"email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b"
},
"description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a Awair \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: https://developer.getawair.com/onboard/login"
}
}
}
}

View File

@ -0,0 +1,29 @@
{
"config": {
"abort": {
"already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"no_devices": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099",
"reauth_successful": "\u5b58\u53d6\u5bc6\u9470 \u5df2\u6210\u529f\u66f4\u65b0"
},
"error": {
"auth": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548",
"unknown": "\u672a\u77e5 Awair API \u932f\u8aa4\u3002"
},
"step": {
"reauth": {
"data": {
"access_token": "\u5b58\u53d6\u5bc6\u9470",
"email": "\u96fb\u5b50\u90f5\u4ef6"
},
"description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u5bc6\u9470\u3002"
},
"user": {
"data": {
"access_token": "\u5b58\u53d6\u5bc6\u9470",
"email": "\u96fb\u5b50\u90f5\u4ef6"
},
"description": "\u5fc5\u9808\u5148\u8a3b\u518a Awair \u958b\u767c\u8005\u5b58\u53d6\u5bc6\u9470\uff1ahttps://developer.getawair.com/onboard/login"
}
}
}
}

View File

@ -42,7 +42,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add binary sensor from Axis device."""
event = device.api.event[event_id]
if event.CLASS != CLASS_OUTPUT:
if event.CLASS != CLASS_OUTPUT and not (
event.CLASS == CLASS_LIGHT and event.TYPE == "Light"
):
async_add_entities([AxisBinarySensor(event, device)], True)
device.listeners.append(

View File

@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
device = hass.data[AXIS_DOMAIN][config_entry.unique_id]
if not device.option_camera:
if not device.api.vapix.params.image_format:
return
async_add_entities([AxisCamera(device)])

View File

@ -3,6 +3,7 @@ import logging
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
LOGGER = logging.getLogger(__package__)
@ -11,7 +12,6 @@ DOMAIN = "axis"
ATTR_MANUFACTURER = "Axis Communications AB"
CONF_CAMERA = "camera"
CONF_EVENTS = "events"
CONF_MODEL = "model"
CONF_STREAM_PROFILE = "stream_profile"
@ -20,4 +20,4 @@ DEFAULT_EVENTS = True
DEFAULT_STREAM_PROFILE = "No stream profile"
DEFAULT_TRIGGER_TIME = 0
PLATFORMS = [BINARY_SENSOR_DOMAIN, CAMERA_DOMAIN, SWITCH_DOMAIN]
PLATFORMS = [BINARY_SENSOR_DOMAIN, CAMERA_DOMAIN, LIGHT_DOMAIN, SWITCH_DOMAIN]

View File

@ -29,7 +29,6 @@ from homeassistant.setup import async_when_setup
from .const import (
ATTR_MANUFACTURER,
CONF_CAMERA,
CONF_EVENTS,
CONF_MODEL,
CONF_STREAM_PROFILE,
@ -78,12 +77,6 @@ class AxisNetworkDevice:
"""Return the serial number of this device."""
return self.config_entry.unique_id
@property
def option_camera(self):
"""Config entry option defining if camera should be used."""
supported_formats = self.api.vapix.params.image_format
return self.config_entry.options.get(CONF_CAMERA, bool(supported_formats))
@property
def option_events(self):
"""Config entry option defining if platforms based on events should be created."""

View File

@ -0,0 +1,116 @@
"""Support for Axis lights."""
from axis.event_stream import CLASS_LIGHT
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS,
LightEntity,
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .axis_base import AxisEventBase
from .const import DOMAIN as AXIS_DOMAIN
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Axis light."""
device = hass.data[AXIS_DOMAIN][config_entry.unique_id]
if not device.api.vapix.light_control:
return
@callback
def async_add_sensor(event_id):
"""Add light from Axis device."""
event = device.api.event[event_id]
if event.CLASS == CLASS_LIGHT and event.TYPE == "Light":
async_add_entities([AxisLight(event, device)], True)
device.listeners.append(
async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor)
)
class AxisLight(AxisEventBase, LightEntity):
"""Representation of a light Axis event."""
def __init__(self, event, device):
"""Initialize the Axis light."""
super().__init__(event, device)
self.light_id = f"led{self.event.id}"
self.current_intensity = 0
self.max_intensity = 0
self._features = SUPPORT_BRIGHTNESS
async def async_added_to_hass(self) -> None:
"""Subscribe lights events."""
await super().async_added_to_hass()
def get_light_capabilities():
"""Get light capabilities."""
current_intensity = self.device.api.vapix.light_control.get_current_intensity(
self.light_id
)
self.current_intensity = current_intensity["data"]["intensity"]
max_intensity = self.device.api.vapix.light_control.get_valid_intensity(
self.light_id
)
self.max_intensity = max_intensity["data"]["ranges"][0]["high"]
await self.hass.async_add_executor_job(get_light_capabilities)
@property
def supported_features(self):
"""Flag supported features."""
return self._features
@property
def name(self):
"""Return the name of the light."""
light_type = self.device.api.vapix.light_control[self.light_id].light_type
return f"{self.device.name} {light_type} {self.event.TYPE} {self.event.id}"
@property
def is_on(self):
"""Return true if light is on."""
return self.event.is_tripped
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return int((self.current_intensity / self.max_intensity) * 255)
def turn_on(self, **kwargs):
"""Turn on light."""
if not self.is_on:
self.device.api.vapix.light_control.activate_light(self.light_id)
if ATTR_BRIGHTNESS in kwargs:
intensity = int((kwargs[ATTR_BRIGHTNESS] / 255) * self.max_intensity)
self.device.api.vapix.light_control.set_manual_intensity(
self.light_id, intensity
)
def turn_off(self, **kwargs):
"""Turn off light."""
if self.is_on:
self.device.api.vapix.light_control.deactivate_light(self.light_id)
def update(self):
"""Update brightness."""
current_intensity = self.device.api.vapix.light_control.get_current_intensity(
self.light_id
)
self.current_intensity = current_intensity["data"]["intensity"]
@property
def should_poll(self):
"""Brightness needs polling."""
return True

View File

@ -3,7 +3,7 @@
"name": "Axis",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/axis",
"requirements": ["axis==31"],
"requirements": ["axis==33"],
"zeroconf": ["_axis-video._tcp.local."],
"after_dependencies": ["mqtt"],
"codeowners": ["@Kane610"]

View File

@ -8,7 +8,7 @@
},
"error": {
"already_configured": "El dispositivo ya est\u00e1 configurado",
"already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en curso.",
"already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en marcha.",
"device_unavailable": "El dispositivo no est\u00e1 disponible",
"faulty_credentials": "Credenciales de usuario incorrectas"
},

View File

@ -12,6 +12,7 @@ _LOGGER = logging.getLogger(__name__)
SUPPORTED_LANGUAGES = ["zh"]
DEFAULT_LANG = "zh"
SUPPORTED_PERSON = [0, 1, 3, 4, 5, 103, 106, 110, 111]
CONF_APP_ID = "app_id"
CONF_SECRET_KEY = "secret_key"
@ -35,9 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_VOLUME, default=5): vol.All(
vol.Coerce(int), vol.Range(min=0, max=15)
),
vol.Optional(CONF_PERSON, default=0): vol.All(
vol.Coerce(int), vol.Range(min=0, max=4)
),
vol.Optional(CONF_PERSON, default=0): vol.In(SUPPORTED_PERSON),
}
)

View File

@ -12,6 +12,7 @@
"step": {
"user": {
"data": {
"host": "Adresse IP",
"port": "Port"
},
"description": "Configurez votre BleBox pour l'int\u00e9grer \u00e0 Home Assistant.",

View File

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

View File

@ -14,6 +14,7 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from .const import (
@ -58,7 +59,7 @@ def _blink_startup_wrapper(entry):
no_prompt=True,
device_id=DEVICE_ID,
)
blink.refresh_rate = entry.data[CONF_SCAN_INTERVAL]
blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
try:
blink.login_response = entry.data["login_response"]
@ -91,6 +92,8 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Set up Blink via config entry."""
_async_import_options_from_data_if_missing(hass, entry)
hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job(
_blink_startup_wrapper, entry
)
@ -130,6 +133,16 @@ async def async_setup_entry(hass, entry):
return True
@callback
def _async_import_options_from_data_if_missing(hass, entry):
options = dict(entry.options)
if CONF_SCAN_INTERVAL not in entry.options:
options[CONF_SCAN_INTERVAL] = entry.data.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
)
hass.config_entries.async_update_entry(entry, options=options)
async def async_unload_entry(hass, entry):
"""Unload Blink entry."""
unload_ok = all(

View File

@ -11,6 +11,7 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
from homeassistant.core import callback
from .const import DEFAULT_OFFSET, DEFAULT_SCAN_INTERVAL, DEVICE_ID, DOMAIN
@ -40,10 +41,15 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.data = {
CONF_USERNAME: "",
CONF_PASSWORD: "",
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
"login_response": None,
}
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get options flow for this handler."""
return BlinkOptionsFlowHandler(config_entry)
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
errors = {}
@ -54,7 +60,7 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(self.data[CONF_USERNAME])
if CONF_SCAN_INTERVAL in user_input:
self.data[CONF_SCAN_INTERVAL] = user_input["scan_interval"]
self.data[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL]
self.blink = Blink(
username=self.data[CONF_USERNAME],
@ -107,6 +113,40 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_user(import_data)
class BlinkOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Blink options."""
def __init__(self, config_entry):
"""Initialize Blink options flow."""
self.config_entry = config_entry
self.options = dict(config_entry.options)
self.blink = None
async def async_step_init(self, user_input=None):
"""Manage the Blink options."""
self.blink = self.hass.data[DOMAIN][self.config_entry.entry_id]
self.options[CONF_SCAN_INTERVAL] = self.blink.refresh_rate
return await self.async_step_simple_options()
async def async_step_simple_options(self, user_input=None):
"""For simple options."""
if user_input is not None:
self.options.update(user_input)
self.blink.refresh_rate = user_input[CONF_SCAN_INTERVAL]
return self.async_create_entry(title="", data=self.options)
options = self.config_entry.options
scan_interval = options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
return self.async_show_form(
step_id="simple_options",
data_schema=vol.Schema(
{vol.Optional(CONF_SCAN_INTERVAL, default=scan_interval,): int}
),
)
class Require2FA(exceptions.HomeAssistantError):
"""Error to indicate we require 2FA."""

View File

@ -21,5 +21,16 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
},
"options": {
"step": {
"simple_options": {
"data": {
"scan_interval": "Scan Interval (seconds)"
},
"title": "Blink options",
"description": "Configure Blink integration"
}
}
}
}

View File

@ -29,7 +29,9 @@
"simple_options": {
"data": {
"scan_interval": "Interval d'escaneig (segons)"
}
},
"description": "Configura la integraci\u00f3 Blink",
"title": "Opcions de Blink"
}
}
}

View File

@ -1,5 +1,8 @@
{
"config": {
"abort": {
"already_configured": "P\u00e9riph\u00e9rique d\u00e9j\u00e0 configur\u00e9"
},
"error": {
"invalid_auth": "Authentification invalide",
"unknown": "Erreur inattendue"
@ -20,5 +23,15 @@
"title": "Connectez-vous avec un compte Blink"
}
}
},
"options": {
"step": {
"simple_options": {
"data": {
"scan_interval": "Intervalle de balayage (secondes)"
},
"title": "Options de clignotement"
}
}
}
}

View File

@ -23,5 +23,16 @@
"title": "Mam Blink Kont verbannen"
}
}
},
"options": {
"step": {
"simple_options": {
"data": {
"scan_interval": "Scan Intervall (sekonnen)"
},
"description": "Blink Integratioun ariichten",
"title": "Blink Optiounen"
}
}
}
}

View File

@ -41,6 +41,7 @@ _SERVICE_MAP = {
"light_flash": "trigger_remote_light_flash",
"sound_horn": "trigger_remote_horn",
"activate_air_conditioning": "trigger_remote_air_conditioning",
"find_vehicle": "trigger_remote_vehicle_finder",
}

View File

@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": ["bimmer_connected==0.7.5"],
"requirements": ["bimmer_connected==0.7.7"],
"dependencies": [],
"codeowners": ["@gerard33"]
"codeowners": ["@gerard33", "@rikroe"]
}

View File

@ -35,6 +35,16 @@ activate_air_conditioning:
The vehicle identification number (VIN) of the vehicle, 17 characters
example: WBANXXXXXX1234567
find_vehicle:
description: >
Request vehicle to update the gps location. The vehicle is identified via the vin
(see below).
fields:
vin:
description: >
The vehicle identification number (VIN) of the vehicle, 17 characters
example: WBANXXXXXX1234567
update_state:
description: >
Fetch the last state of the vehicles of all your accounts from the BMW

View File

@ -19,7 +19,7 @@
},
"user": {
"data": {
"host": "TV-vertsnavn eller IP-adresse"
"host": "Vert"
},
"description": "Sett opp Sony Bravia TV-integrasjon. Hvis du har problemer med konfigurasjonen, g\u00e5 til: [https://www.home-assistant.io/integrations/braviatv](https://www.home-assistant.io/integrations/braviatv)\n\n Forsikre deg om at TV-en er sl\u00e5tt p\u00e5.",
"title": ""

View File

@ -2,7 +2,6 @@
import asyncio
from base64 import b64decode, b64encode
from binascii import unhexlify
from datetime import timedelta
import logging
import re
@ -13,7 +12,7 @@ from homeassistant.const import CONF_HOST
import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow
from .const import CONF_PACKET, DOMAIN, SERVICE_LEARN, SERVICE_SEND
from .const import CONF_PACKET, DOMAIN, LEARNING_TIMEOUT, SERVICE_LEARN, SERVICE_SEND
_LOGGER = logging.getLogger(__name__)
@ -84,7 +83,7 @@ async def async_setup_service(hass, host, device):
_LOGGER.info("Press the key you want Home Assistant to learn")
start_time = utcnow()
while (utcnow() - start_time) < timedelta(seconds=20):
while (utcnow() - start_time) < LEARNING_TIMEOUT:
await asyncio.sleep(1)
try:
packet = await device.async_request(device.api.check_data)

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