mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Merge pull request #37280 from home-assistant/rc
This commit is contained in:
commit
dc8bfb76dc
51
.coveragerc
51
.coveragerc
@ -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
2
.gitattributes
vendored
@ -8,3 +8,5 @@
|
||||
*.png binary
|
||||
*.zip binary
|
||||
*.mp3 binary
|
||||
|
||||
Dockerfile.dev linguist-language=Dockerfile
|
||||
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -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:
|
||||
|
||||
|
2
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
2
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
@ -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:
|
||||
|
||||
|
18
CODEOWNERS
18
CODEOWNERS
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "Abode"
|
||||
}
|
@ -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"
|
||||
)
|
||||
|
@ -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],
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
@ -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": {
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"latitude": "Zemepisn\u00e1 \u0161\u00edrka",
|
||||
"longitude": "Zemepisn\u00e1 d\u013a\u017eka"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
28
homeassistant/components/alexa/logbook.py
Normal file
28
homeassistant/components/alexa/logbook.py
Normal 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)
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "Almond"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "Ambiclimate"
|
||||
}
|
@ -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),
|
||||
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "Ambient PWS"
|
||||
}
|
@ -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."""
|
||||
|
@ -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,
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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})
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "Arcam FMJ"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "Arcam FMJ"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "Arcam FMJ"
|
||||
}
|
@ -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}",
|
||||
|
@ -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"
|
||||
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "Arcam FMJ"
|
||||
}
|
@ -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"
|
||||
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "Arcam FMJ"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"title": "Arcam FMJ"
|
||||
}
|
@ -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
|
||||
)
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
23
homeassistant/components/automation/logbook.py
Normal file
23
homeassistant/components/automation/logbook.py
Normal 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
|
||||
)
|
@ -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"
|
||||
}
|
||||
|
@ -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(
|
||||
|
24
homeassistant/components/avri/.translations/en.json
Normal file
24
homeassistant/components/avri/.translations/en.json
Normal 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"
|
||||
}
|
24
homeassistant/components/avri/.translations/nl.json
Normal file
24
homeassistant/components/avri/.translations/nl.json
Normal 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"
|
||||
}
|
@ -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
|
||||
|
74
homeassistant/components/avri/config_flow.py
Normal file
74
homeassistant/components/avri/config_flow.py
Normal 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],
|
||||
},
|
||||
)
|
8
homeassistant/components/avri/const.py
Normal file
8
homeassistant/components/avri/const.py
Normal 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"
|
@ -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
|
||||
}
|
@ -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:
|
||||
|
24
homeassistant/components/avri/strings.json
Normal file
24
homeassistant/components/avri/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
homeassistant/components/avri/translations/ar.json
Normal file
7
homeassistant/components/avri/translations/ar.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
@ -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": {
|
||||
|
@ -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)
|
||||
|
109
homeassistant/components/awair/config_flow.py
Normal file
109
homeassistant/components/awair/config_flow.py
Normal 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")
|
120
homeassistant/components/awair/const.py
Normal file
120
homeassistant/components/awair/const.py
Normal 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
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
29
homeassistant/components/awair/strings.json
Normal file
29
homeassistant/components/awair/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
28
homeassistant/components/awair/translations/ca.json
Normal file
28
homeassistant/components/awair/translations/ca.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
homeassistant/components/awair/translations/en.json
Normal file
29
homeassistant/components/awair/translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
homeassistant/components/awair/translations/es.json
Normal file
29
homeassistant/components/awair/translations/es.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/awair/translations/no.json
Normal file
21
homeassistant/components/awair/translations/no.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
homeassistant/components/awair/translations/ru.json
Normal file
29
homeassistant/components/awair/translations/ru.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
homeassistant/components/awair/translations/zh-Hant.json
Normal file
29
homeassistant/components/awair/translations/zh-Hant.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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)])
|
||||
|
@ -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]
|
||||
|
@ -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."""
|
||||
|
116
homeassistant/components/axis/light.py
Normal file
116
homeassistant/components/axis/light.py
Normal 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
|
@ -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"]
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Adresse IP",
|
||||
"port": "Port"
|
||||
},
|
||||
"description": "Configurez votre BleBox pour l'int\u00e9grer \u00e0 Home Assistant.",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "IP adresse",
|
||||
"port": "Port"
|
||||
},
|
||||
"description": "Konfigurer BleBox-en til \u00e5 integreres med Home Assistant.",
|
||||
|
@ -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(
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,9 @@
|
||||
"simple_options": {
|
||||
"data": {
|
||||
"scan_interval": "Interval d'escaneig (segons)"
|
||||
}
|
||||
},
|
||||
"description": "Configura la integraci\u00f3 Blink",
|
||||
"title": "Opcions de Blink"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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": ""
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user