Merge pull request #37280 from home-assistant/rc

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

View File

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

2
.gitattributes vendored
View File

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

View File

@ -21,7 +21,7 @@
- Home Assistant Core release with the issue: - Home Assistant Core release with the issue:
- Last working Home Assistant Core release (if known): - 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: - Integration causing this issue:
- Link to integration documentation on our website: - Link to integration documentation on our website:

View File

@ -25,7 +25,7 @@ about: Report an issue with Home Assistant Core
- Home Assistant Core release with the issue: - Home Assistant Core release with the issue:
- Last working Home Assistant Core release (if known): - 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: - Integration causing this issue:
- Link to integration documentation on our website: - Link to integration documentation on our website:

View File

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

View File

@ -117,7 +117,8 @@ class TotpAuthModule(MultiFactorAuthModule):
Mfa module should extend SetupFlow 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) return TotpSetupFlow(self, self.input_schema, user)
async def async_setup_user(self, user_id: str, setup_data: Any) -> str: async def async_setup_user(self, user_id: str, setup_data: Any) -> str:

View File

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

View File

@ -1,6 +1,7 @@
"""Provide methods to bootstrap a Home Assistant instance.""" """Provide methods to bootstrap a Home Assistant instance."""
import asyncio import asyncio
import contextlib import contextlib
from datetime import datetime
import logging import logging
import logging.handlers import logging.handlers
import os import os
@ -20,7 +21,12 @@ from homeassistant.const import (
) )
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType 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.logging import async_activate_log_queue_handler
from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache from homeassistant.util.yaml import clear_secret_cache
@ -34,12 +40,18 @@ DATA_LOGGING = "logging"
LOG_SLOW_STARTUP_INTERVAL = 60 LOG_SLOW_STARTUP_INTERVAL = 60
DEBUGGER_INTEGRATIONS = {"ptvsd"} DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"}
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
LOGGING_INTEGRATIONS = {"logger", "system_log", "sentry"} LOGGING_INTEGRATIONS = {
STAGE_1_INTEGRATIONS = { # Set log levels
"logger",
# Error logging
"system_log",
"sentry",
# To record data # To record data
"recorder", "recorder",
}
STAGE_1_INTEGRATIONS = {
# To make sure we forward data to other instances # To make sure we forward data to other instances
"mqtt_eventstream", "mqtt_eventstream",
# To provide account link implementations # To provide account link implementations
@ -50,7 +62,6 @@ STAGE_1_INTEGRATIONS = {
# as possible so problem integrations can # as possible so problem integrations can
# be removed # be removed
"frontend", "frontend",
"config",
} }
@ -125,8 +136,12 @@ async def async_setup_hass(
await hass.async_block_till_done() await hass.async_block_till_done()
safe_mode = True safe_mode = True
old_config = hass.config
hass = core.HomeAssistant() 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: if safe_mode:
_LOGGER.info("Starting in 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 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( async def _async_set_up_integrations(
hass: core.HomeAssistant, config: Dict[str, Any] hass: core.HomeAssistant, config: Dict[str, Any]
) -> None: ) -> None:
"""Set up all the integrations.""" """Set up all the integrations."""
setup_started = hass.data[DATA_SETUP_STARTED] = {} setup_started = hass.data[DATA_SETUP_STARTED] = {}
domains_to_setup = _get_domains(hass, config)
async def async_setup_multi_components(domains: Set[str]) -> None: # Resolve all dependencies so we know all integrations
"""Set up multiple domains. Log on failure.""" # 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: integrations_to_process = [
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" int_or_exc
while True: for int_or_exc in await asyncio.gather(
await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL) *(
remaining = [domain for domain in domains if domain in setup_started] loader.async_get_integration(hass, domain)
for domain in old_to_resolve
if remaining: ),
_LOGGER.info( return_exceptions=True,
"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__),
) )
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. # Start up debuggers. Start these first in case they want to wait.
debuggers = domains & DEBUGGER_INTEGRATIONS debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS
if debuggers: if debuggers:
_LOGGER.debug("Starting up debuggers %s", debuggers) _LOGGER.debug("Setting up debuggers: %s", debuggers)
await async_setup_multi_components(debuggers) await async_setup_multi_components(hass, debuggers, config, setup_started)
domains -= DEBUGGER_INTEGRATIONS
# Resolve all dependencies of all components so we can find the logging # calculate what components to setup in what stage
# and integrations that need faster initialization. stage_1_domains = set()
resolved_domains_task = asyncio.gather(
*(loader.async_component_dependencies(hass, domain) for domain in domains),
return_exceptions=True,
)
# Finish resolving domains # Find all dependencies of any dependency of any stage 1 integration that
for dep_domains in await resolved_domains_task: # we plan on loading and promote them to stage 1
# Result is either a set or an exception. We ignore exceptions deps_promotion = STAGE_1_INTEGRATIONS
# It will be properly handled during setup of the domain. while deps_promotion:
if isinstance(dep_domains, set): old_deps_promotion = deps_promotion
domains.update(dep_domains) deps_promotion = set()
# setup components for domain in old_deps_promotion:
logging_domains = domains & LOGGING_INTEGRATIONS if domain not in domains_to_setup or domain in stage_1_domains:
stage_1_domains = domains & STAGE_1_INTEGRATIONS continue
stage_2_domains = domains - logging_domains - stage_1_domains
if logging_domains: stage_1_domains.add(domain)
_LOGGER.info("Setting up %s", logging_domains)
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. # Kick off loading the registries. They don't need to be awaited.
asyncio.gather( asyncio.gather(
@ -405,49 +474,17 @@ async def _async_set_up_integrations(
hass.helpers.area_registry.async_get_registry(), hass.helpers.area_registry.async_get_registry(),
) )
# Start setup
if stage_1_domains: 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: if stage_2_domains:
_LOGGER.debug("Final set up: %s", 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)
await async_setup_multi_components(stage_2_domains)
# Wrap up startup # Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up") _LOGGER.debug("Waiting for startup to wrap up")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
"already_configured": "El dispositivo ya est\u00e1 configurado" "already_configured": "El dispositivo ya est\u00e1 configurado"
}, },
"error": { "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" "device_unavailable": "El dispositivo no est\u00e1 disponible"
}, },
"step": { "step": {

View File

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

View File

@ -21,7 +21,7 @@
"node_pro": { "node_pro": {
"data": { "data": {
"ip_address": "Enhetens IP-adresse / vertsnavn", "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.", "description": "Overv\u00e5ke en personlig AirVisual-enhet. Passordet kan hentes fra enhetens brukergrensesnitt.",
"title": "Konfigurer en AirVisual Node / Pro" "title": "Konfigurer en AirVisual Node / Pro"

View File

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

View File

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

View File

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

View File

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

View File

@ -222,11 +222,6 @@ class Alert(ToggleEntity):
return STATE_ON return STATE_ON
return STATE_IDLE 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): async def watched_entity_change(self, entity, from_state, to_state):
"""Determine if the alert should start or stop.""" """Determine if the alert should start or stop."""
_LOGGER.debug("Watched entity (%s) has changed", entity) _LOGGER.debug("Watched entity (%s) has changed", entity)
@ -310,7 +305,9 @@ class Alert(ToggleEntity):
_LOGGER.debug(msg_payload) _LOGGER.debug(msg_payload)
for target in self._notifiers: 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 def async_turn_on(self, **kwargs):
"""Async Unacknowledge alert.""" """Async Unacknowledge alert."""

View File

@ -4,7 +4,6 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_NAME 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 homeassistant.helpers import config_validation as cv, entityfilter
from . import flash_briefings, intent, smart_home_http from . import flash_briefings, intent, smart_home_http
@ -17,12 +16,12 @@ from .const import (
CONF_ENTITY_CONFIG, CONF_ENTITY_CONFIG,
CONF_FILTER, CONF_FILTER,
CONF_LOCALE, CONF_LOCALE,
CONF_PASSWORD,
CONF_SUPPORTED_LOCALES, CONF_SUPPORTED_LOCALES,
CONF_TEXT, CONF_TEXT,
CONF_TITLE, CONF_TITLE,
CONF_UID, CONF_UID,
DOMAIN, DOMAIN,
EVENT_ALEXA_SMART_HOME,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -56,6 +55,7 @@ CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: { DOMAIN: {
CONF_FLASH_BRIEFINGS: { CONF_FLASH_BRIEFINGS: {
vol.Required(CONF_PASSWORD): cv.string,
cv.string: vol.All( cv.string: vol.All(
cv.ensure_list, cv.ensure_list,
[ [
@ -67,7 +67,7 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_DISPLAY_URL): cv.template, vol.Optional(CONF_DISPLAY_URL): cv.template,
} }
], ],
) ),
}, },
# vol.Optional here would mean we couldn't distinguish between an empty # vol.Optional here would mean we couldn't distinguish between an empty
# smart_home: and none at all. # smart_home: and none at all.
@ -80,28 +80,6 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass, config): async def async_setup(hass, config):
"""Activate the Alexa component.""" """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: if DOMAIN not in config:
return True return True

View File

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

View File

@ -1,15 +1,17 @@
"""Support for Alexa skill service end point.""" """Support for Alexa skill service end point."""
import copy import copy
import hmac
import logging import logging
import uuid import uuid
from homeassistant.components import http 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.core import callback
from homeassistant.helpers import template from homeassistant.helpers import template
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .const import ( from .const import (
API_PASSWORD,
ATTR_MAIN_TEXT, ATTR_MAIN_TEXT,
ATTR_REDIRECTION_URL, ATTR_REDIRECTION_URL,
ATTR_STREAM_URL, ATTR_STREAM_URL,
@ -18,6 +20,7 @@ from .const import (
ATTR_UPDATE_DATE, ATTR_UPDATE_DATE,
CONF_AUDIO, CONF_AUDIO,
CONF_DISPLAY_URL, CONF_DISPLAY_URL,
CONF_PASSWORD,
CONF_TEXT, CONF_TEXT,
CONF_TITLE, CONF_TITLE,
CONF_UID, CONF_UID,
@ -39,6 +42,7 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
"""Handle Alexa Flash Briefing skill requests.""" """Handle Alexa Flash Briefing skill requests."""
url = FLASH_BRIEFINGS_API_ENDPOINT url = FLASH_BRIEFINGS_API_ENDPOINT
requires_auth = False
name = "api:alexa:flash_briefings" name = "api:alexa:flash_briefings"
def __init__(self, hass, flash_briefings): def __init__(self, hass, flash_briefings):
@ -52,7 +56,20 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
"""Handle Alexa Flash Briefing request.""" """Handle Alexa Flash Briefing request."""
_LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id) _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" err = "No configured Alexa flash briefing was found for: %s"
_LOGGER.error(err, briefing_id) _LOGGER.error(err, briefing_id)
return b"", HTTP_NOT_FOUND return b"", HTTP_NOT_FOUND

View File

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

View File

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

View File

@ -94,12 +94,12 @@ class AlmondFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler):
data={"type": TYPE_LOCAL, "host": user_input["host"]}, 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.""" """Receive a Hass.io discovery."""
if self._async_current_entries(): if self._async_current_entries():
return self.async_abort(reason="already_setup") return self.async_abort(reason="already_setup")
self.hassio_discovery = user_input self.hassio_discovery = discovery_info
return await self.async_step_hassio_confirm() return await self.async_step_hassio_confirm()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,27 +5,15 @@ import logging
from arcam.fmj import ConnectionFailed from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client from arcam.fmj.client import Client
import async_timeout import async_timeout
import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import ( from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
CONF_HOST,
CONF_NAME,
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_ZONE,
EVENT_HOMEASSISTANT_STOP,
SERVICE_TURN_ON,
)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from .const import ( from .const import (
DEFAULT_NAME,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
DOMAIN, DOMAIN,
DOMAIN_DATA_CONFIG,
DOMAIN_DATA_ENTRIES, DOMAIN_DATA_ENTRIES,
DOMAIN_DATA_TASKS, DOMAIN_DATA_TASKS,
SIGNAL_CLIENT_DATA, SIGNAL_CLIENT_DATA,
@ -35,44 +23,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.115")
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,
)
)
async def _await_cancel(task): async def _await_cancel(task):
@ -83,27 +34,10 @@ async def _await_cancel(task):
pass 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): async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up the component.""" """Set up the component."""
hass.data[DOMAIN_DATA_ENTRIES] = {} hass.data[DOMAIN_DATA_ENTRIES] = {}
hass.data[DOMAIN_DATA_TASKS] = {} 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(_): async def _stop(_):
asyncio.gather( 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): 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]) client = Client(entry.data[CONF_HOST], entry.data[CONF_PORT])
entries[entry.entry_id] = client
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,
}
task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL)) task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL))
tasks[entry.entry_id] = task tasks[entry.entry_id] = task

View File

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

View File

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

View File

@ -1,8 +1,14 @@
{ {
"domain": "arcam_fmj", "domain": "arcam_fmj",
"name": "Arcam FMJ Receivers", "name": "Arcam FMJ Receivers",
"config_flow": false, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj", "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"] "codeowners": ["@elupus"]
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "El dispositivo ya est\u00e1 configurado.", "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." "unable_to_connect": "No se puede conectar con el dispositivo."
}, },
"flow_title": "Arcam FMJ en {host}", "flow_title": "Arcam FMJ en {host}",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{ {
"config": { "config": {
"abort": { "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": { "error": {
"connection_error": "Verbindung fehlgeschlagen, versuchen Sie es erneut" "connection_error": "Verbindung fehlgeschlagen, versuchen Sie es erneut"

View File

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

View File

@ -222,19 +222,6 @@ async def async_setup(hass, config):
hass, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) 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 return True

View File

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

View File

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

View File

@ -19,7 +19,7 @@ DEFAULT_QOS = 0
TRIGGER_SCHEMA = vol.Schema( TRIGGER_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_PLATFORM): mqtt.DOMAIN, 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_PAYLOAD): cv.string,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All( vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All(

View File

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

View File

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

View File

@ -1 +1,63 @@
"""The avri component.""" """The avri component."""
import asyncio
from datetime import timedelta
import logging
from avri.api import Avri
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import (
CONF_COUNTRY_CODE,
CONF_HOUSE_NUMBER,
CONF_HOUSE_NUMBER_EXTENSION,
CONF_ZIP_CODE,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor"]
SCAN_INTERVAL = timedelta(hours=4)
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Avri component."""
hass.data[DOMAIN] = {}
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Avri from a config entry."""
client = Avri(
postal_code=entry.data[CONF_ZIP_CODE],
house_nr=entry.data[CONF_HOUSE_NUMBER],
house_nr_extension=entry.data.get(CONF_HOUSE_NUMBER_EXTENSION),
country_code=entry.data[CONF_COUNTRY_CODE],
)
hass.data[DOMAIN][entry.entry_id] = client
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,248 +1,245 @@
"""Support for the Awair indoor air quality monitor.""" """Support for Awair sensors."""
from datetime import timedelta from typing import Callable, List, Optional
import logging
import math
from python_awair import AwairClient from python_awair.devices import AwairDevice
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.components.awair import AwairDataUpdateCoordinator, AwairResult
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, from homeassistant.components.sensor import PLATFORM_SCHEMA
CONCENTRATION_PARTS_PER_BILLION, from homeassistant.config_entries import SOURCE_IMPORT
CONCENTRATION_PARTS_PER_MILLION, from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_ACCESS_TOKEN
CONF_ACCESS_TOKEN, from homeassistant.helpers import device_registry as dr
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
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity 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" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
ATTR_TIMESTAMP = "timestamp" {vol.Required(CONF_ACCESS_TOKEN): cv.string}, extra=vol.ALLOW_EXTRA,
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]),
}
) )
# 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): 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: async def async_setup_entry(
all_devices = [] hass: HomeAssistantType,
devices = config.get(CONF_DEVICES, await client.devices()) 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. data: List[AwairResult] = coordinator.data.values()
throttle_minutes = math.ceil(60 / ((AWAIR_QUOTA / len(devices)) / 24)) for result in data:
throttle = timedelta(minutes=throttle_minutes) 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: # The "DUST" sensor for Awair is a combo pm2.5/pm10 sensor only
_LOGGER.debug("Found awair device: %s", device) # present on first-gen devices in lieu of separate pm2.5/pm10 sensors.
awair_data = AwairData(client, device[CONF_UUID], throttle) # We handle that by creating fake pm2.5/pm10 sensors that will always
await awair_data.async_update() # report identical values, and we let users decide how they want to use
for sensor in SENSOR_TYPES: # that data - because we can't really tell what kind of particles the
if sensor in awair_data.data: # "DUST" sensor actually detected. However, it's still useful data.
awair_sensor = AwairSensor(awair_data, device, sensor, throttle) if API_DUST in device_sensors:
all_devices.append(awair_sensor) for alias_kind in DUST_ALIASES:
sensors.append(AwairSensor(alias_kind, result.device, coordinator))
async_add_entities(all_devices, True) async_add_entities(sensors)
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
class AwairSensor(Entity): class AwairSensor(Entity):
"""Implementation of an Awair device.""" """Defines an Awair sensor entity."""
def __init__(self, data, device, sensor_type, throttle): def __init__(
"""Initialize the sensor.""" self, kind: str, device: AwairDevice, coordinator: AwairDataUpdateCoordinator,
self._uuid = device[CONF_UUID] ) -> None:
self._device_class = SENSOR_TYPES[sensor_type]["device_class"] """Set up an individual AwairSensor."""
self._name = f"Awair {self._device_class}" self._kind = kind
unit = SENSOR_TYPES[sensor_type]["unit_of_measurement"] self._device = device
self._unit_of_measurement = unit self._coordinator = coordinator
self._data = data
self._type = sensor_type
self._throttle = throttle
@property @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 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 @property
def device_class(self): def unique_id(self) -> str:
"""Return the device class.""" """Return the uuid as the unique_id."""
return self._device_class 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 @property
def icon(self): def available(self) -> bool:
"""Icon to use in the frontend.""" """Determine if the sensor is available based on API results."""
return SENSOR_TYPES[self._type]["icon"] # 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 @property
def state(self): def state(self) -> float:
"""Return the state of the device.""" """Return the state, rounding off to reasonable values."""
return self._data.data[self._type] 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 @property
def device_state_attributes(self): def icon(self) -> str:
"""Return additional attributes.""" """Return the icon."""
return self._data.attrs return SENSOR_TYPES[self._kind][ATTR_ICON]
# 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)
@property @property
def unique_id(self): def device_class(self) -> str:
"""Return the unique id of this entity.""" """Return the device_class."""
return f"{self._uuid}_{self._type}" return SENSOR_TYPES[self._kind][ATTR_DEVICE_CLASS]
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str:
"""Return the unit of measurement of this entity.""" """Return the unit the value is expressed in."""
return self._unit_of_measurement return SENSOR_TYPES[self._kind][ATTR_UNIT]
async def async_update(self): @property
"""Get the latest data.""" def device_state_attributes(self) -> dict:
await self._data.async_update() """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: 0: green
"""Get data from Awair API.""" 1: yellow
2: light-orange
3: orange
4: red
def __init__(self, client, uuid, throttle): The API indicates that both positive and negative values may be returned,
"""Initialize the data object.""" but the negative values are mapped to identical colors as the positive values.
self._client = client Knowing that, we just return the absolute value of a given index so that
self._uuid = uuid users don't have to handle positive/negative values that ultimately "mean"
self.data = {} the same thing.
self.attrs = {}
self.async_update = Throttle(throttle)(self._async_update)
async def _async_update(self): https://docs.developer.getawair.com/?version=latest#awair-score-and-index
"""Get the data from Awair API.""" """
resp = await self._client.air_data_latest(self._uuid) 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 attrs
return
timestamp = dt.parse_datetime(resp[0][ATTR_TIMESTAMP]) @property
self.attrs[ATTR_LAST_API_UPDATE] = timestamp def device_info(self) -> dict:
self.data[ATTR_SCORE] = resp[0][ATTR_SCORE] """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 if self._device.name:
# be safe to only process one entry. info["name"] = self._device.name
for sensor in resp[0][ATTR_SENSORS]:
self.data[sensor[ATTR_COMPONENT]] = round(sensor[ATTR_VALUE], 1)
_LOGGER.debug("Got Awair Data for %s: %s", self._uuid, self.data) if self._device.mac_address:
info["connections"] = {
(dr.CONNECTION_NETWORK_MAC, self._device.mac_address)
}
return info
async def async_added_to_hass(self) -> None:
"""Connect to dispatcher listening for entity data notifications."""
self.async_on_remove(
self._coordinator.async_add_listener(self.async_write_ha_state)
)
async def async_update(self) -> None:
"""Update Awair entity."""
await self._coordinator.async_request_refresh()
@property
def _air_data(self) -> Optional[AwairResult]:
"""Return the latest data for our device, or None."""
result: Optional[AwairResult] = self._coordinator.data.get(self._device.uuid)
if result:
return result.air_data
return None

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -42,7 +42,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add binary sensor from Axis device.""" """Add binary sensor from Axis device."""
event = device.api.event[event_id] 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) async_add_entities([AxisBinarySensor(event, device)], True)
device.listeners.append( device.listeners.append(

View File

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

View File

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

View File

@ -29,7 +29,6 @@ from homeassistant.setup import async_when_setup
from .const import ( from .const import (
ATTR_MANUFACTURER, ATTR_MANUFACTURER,
CONF_CAMERA,
CONF_EVENTS, CONF_EVENTS,
CONF_MODEL, CONF_MODEL,
CONF_STREAM_PROFILE, CONF_STREAM_PROFILE,
@ -78,12 +77,6 @@ class AxisNetworkDevice:
"""Return the serial number of this device.""" """Return the serial number of this device."""
return self.config_entry.unique_id 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 @property
def option_events(self): def option_events(self):
"""Config entry option defining if platforms based on events should be created.""" """Config entry option defining if platforms based on events should be created."""

View File

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

View File

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

View File

@ -8,7 +8,7 @@
}, },
"error": { "error": {
"already_configured": "El dispositivo ya est\u00e1 configurado", "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", "device_unavailable": "El dispositivo no est\u00e1 disponible",
"faulty_credentials": "Credenciales de usuario incorrectas" "faulty_credentials": "Credenciales de usuario incorrectas"
}, },

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_USERNAME, CONF_USERNAME,
) )
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from .const import ( from .const import (
@ -58,7 +59,7 @@ def _blink_startup_wrapper(entry):
no_prompt=True, no_prompt=True,
device_id=DEVICE_ID, 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: try:
blink.login_response = entry.data["login_response"] blink.login_response = entry.data["login_response"]
@ -91,6 +92,8 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry): async def async_setup_entry(hass, entry):
"""Set up Blink via config 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( hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job(
_blink_startup_wrapper, entry _blink_startup_wrapper, entry
) )
@ -130,6 +133,16 @@ async def async_setup_entry(hass, entry):
return True 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): async def async_unload_entry(hass, entry):
"""Unload Blink entry.""" """Unload Blink entry."""
unload_ok = all( unload_ok = all(

View File

@ -11,6 +11,7 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_USERNAME, CONF_USERNAME,
) )
from homeassistant.core import callback
from .const import DEFAULT_OFFSET, DEFAULT_SCAN_INTERVAL, DEVICE_ID, DOMAIN from .const import DEFAULT_OFFSET, DEFAULT_SCAN_INTERVAL, DEVICE_ID, DOMAIN
@ -40,10 +41,15 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.data = { self.data = {
CONF_USERNAME: "", CONF_USERNAME: "",
CONF_PASSWORD: "", CONF_PASSWORD: "",
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
"login_response": None, "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): async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user.""" """Handle a flow initiated by the user."""
errors = {} errors = {}
@ -54,7 +60,7 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(self.data[CONF_USERNAME]) await self.async_set_unique_id(self.data[CONF_USERNAME])
if CONF_SCAN_INTERVAL in user_input: 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( self.blink = Blink(
username=self.data[CONF_USERNAME], username=self.data[CONF_USERNAME],
@ -107,6 +113,40 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_user(import_data) 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): class Require2FA(exceptions.HomeAssistantError):
"""Error to indicate we require 2FA.""" """Error to indicate we require 2FA."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
"domain": "bmw_connected_drive", "domain": "bmw_connected_drive",
"name": "BMW Connected Drive", "name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/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": [], "dependencies": [],
"codeowners": ["@gerard33"] "codeowners": ["@gerard33", "@rikroe"]
} }

View File

@ -35,6 +35,16 @@ activate_air_conditioning:
The vehicle identification number (VIN) of the vehicle, 17 characters The vehicle identification number (VIN) of the vehicle, 17 characters
example: WBANXXXXXX1234567 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: update_state:
description: > description: >
Fetch the last state of the vehicles of all your accounts from the BMW Fetch the last state of the vehicles of all your accounts from the BMW

View File

@ -19,7 +19,7 @@
}, },
"user": { "user": {
"data": { "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.", "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": "" "title": ""

View File

@ -2,7 +2,6 @@
import asyncio import asyncio
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from binascii import unhexlify from binascii import unhexlify
from datetime import timedelta
import logging import logging
import re import re
@ -13,7 +12,7 @@ from homeassistant.const import CONF_HOST
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow 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__) _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") _LOGGER.info("Press the key you want Home Assistant to learn")
start_time = utcnow() start_time = utcnow()
while (utcnow() - start_time) < timedelta(seconds=20): while (utcnow() - start_time) < LEARNING_TIMEOUT:
await asyncio.sleep(1) await asyncio.sleep(1)
try: try:
packet = await device.async_request(device.api.check_data) 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