Merge pull request #38176 from home-assistant/rc

This commit is contained in:
Franck Nijhof 2020-07-24 22:20:36 +02:00 committed by GitHub
commit 79b1c3f573
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 581 additions and 192 deletions

View File

@ -278,7 +278,7 @@ 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
homeassistant/components/nuheat/* @bdraco homeassistant/components/nuheat/* @bdraco
homeassistant/components/nuki/* @pvizeli homeassistant/components/nuki/* @pschmitt @pvizeli
homeassistant/components/numato/* @clssn homeassistant/components/numato/* @clssn
homeassistant/components/nut/* @bdraco homeassistant/components/nut/* @bdraco
homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nws/* @MatthewFlamm

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[async]==0.2.0", "adb-shell[async]==0.2.0",
"androidtv[async]==0.0.45", "androidtv[async]==0.0.46",
"pure-python-adb==0.2.2.dev0" "pure-python-adb==0.2.2.dev0"
], ],
"codeowners": ["@JeffLIrion"] "codeowners": ["@JeffLIrion"]

View File

@ -2,6 +2,6 @@
"domain": "discord", "domain": "discord",
"name": "Discord", "name": "Discord",
"documentation": "https://www.home-assistant.io/integrations/discord", "documentation": "https://www.home-assistant.io/integrations/discord",
"requirements": ["discord.py==1.3.3"], "requirements": ["discord.py==1.3.4"],
"codeowners": [] "codeowners": []
} }

View File

@ -2,6 +2,6 @@
"domain": "nuki", "domain": "nuki",
"name": "Nuki", "name": "Nuki",
"documentation": "https://www.home-assistant.io/integrations/nuki", "documentation": "https://www.home-assistant.io/integrations/nuki",
"requirements": ["pynuki==1.3.7"], "requirements": ["pynuki==1.3.8"],
"codeowners": ["@pvizeli"] "codeowners": ["@pschmitt", "@pvizeli"]
} }

View File

@ -194,7 +194,7 @@ def setup_internal(hass, config):
"sub_type": event.device.subtype, "sub_type": event.device.subtype,
"type_string": event.device.type_string, "type_string": event.device.type_string,
"id_string": event.device.id_string, "id_string": event.device.id_string,
"data": "".join(f"{x:02x}" for x in event.data), "data": binascii.hexlify(event.data).decode("ASCII"),
"values": getattr(event, "values", None), "values": getattr(event, "values", None),
} }
@ -339,7 +339,7 @@ def get_device_id(device, data_bits=None):
if data_bits and device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: if data_bits and device.packettype == DEVICE_PACKET_TYPE_LIGHTING4:
masked_id = get_pt2262_deviceid(id_string, data_bits) masked_id = get_pt2262_deviceid(id_string, data_bits)
if masked_id: if masked_id:
id_string = str(masked_id) id_string = masked_id.decode("ASCII")
return (f"{device.packettype:x}", f"{device.subtype:x}", id_string) return (f"{device.packettype:x}", f"{device.subtype:x}", id_string)

View File

@ -7,6 +7,7 @@ from homeassistant.core import callback
from . import ( from . import (
CONF_AUTOMATIC_ADD, CONF_AUTOMATIC_ADD,
CONF_DATA_BITS,
CONF_SIGNAL_REPETITIONS, CONF_SIGNAL_REPETITIONS,
DEFAULT_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS,
SIGNAL_EVENT, SIGNAL_EVENT,
@ -38,7 +39,9 @@ async def async_setup_entry(
if not supported(event): if not supported(event):
continue continue
device_id = get_device_id(event.device) device_id = get_device_id(
event.device, data_bits=entity_info.get(CONF_DATA_BITS)
)
if device_id in device_ids: if device_id in device_ids:
continue continue
device_ids.add(device_id) device_ids.add(device_id)

View File

@ -13,6 +13,7 @@ from homeassistant.core import callback
from . import ( from . import (
CONF_AUTOMATIC_ADD, CONF_AUTOMATIC_ADD,
CONF_DATA_BITS,
CONF_SIGNAL_REPETITIONS, CONF_SIGNAL_REPETITIONS,
DEFAULT_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS,
SIGNAL_EVENT, SIGNAL_EVENT,
@ -50,7 +51,9 @@ async def async_setup_entry(
if not supported(event): if not supported(event):
continue continue
device_id = get_device_id(event.device) device_id = get_device_id(
event.device, data_bits=entity_info.get(CONF_DATA_BITS)
)
if device_id in device_ids: if device_id in device_ids:
continue continue
device_ids.add(device_id) device_ids.add(device_id)

View File

@ -14,6 +14,7 @@ from homeassistant.core import callback
from . import ( from . import (
CONF_AUTOMATIC_ADD, CONF_AUTOMATIC_ADD,
CONF_DATA_BITS,
DATA_TYPES, DATA_TYPES,
SIGNAL_EVENT, SIGNAL_EVENT,
RfxtrxEntity, RfxtrxEntity,
@ -64,7 +65,7 @@ async def async_setup_entry(
return isinstance(event, (ControlEvent, SensorEvent)) return isinstance(event, (ControlEvent, SensorEvent))
entities = [] entities = []
for packet_id in discovery_info[CONF_DEVICES]: for packet_id, entity in discovery_info[CONF_DEVICES].items():
event = get_rfx_object(packet_id) event = get_rfx_object(packet_id)
if event is None: if event is None:
_LOGGER.error("Invalid device: %s", packet_id) _LOGGER.error("Invalid device: %s", packet_id)
@ -72,7 +73,7 @@ async def async_setup_entry(
if not supported(event): if not supported(event):
continue continue
device_id = get_device_id(event.device) device_id = get_device_id(event.device, data_bits=entity.get(CONF_DATA_BITS))
for data_type in set(event.values) & set(DATA_TYPES): for data_type in set(event.values) & set(DATA_TYPES):
data_id = (*device_id, data_type) data_id = (*device_id, data_type)
if data_id in data_ids: if data_id in data_ids:

View File

@ -9,6 +9,7 @@ from homeassistant.core import callback
from . import ( from . import (
CONF_AUTOMATIC_ADD, CONF_AUTOMATIC_ADD,
CONF_DATA_BITS,
CONF_SIGNAL_REPETITIONS, CONF_SIGNAL_REPETITIONS,
DEFAULT_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS,
DOMAIN, DOMAIN,
@ -48,7 +49,9 @@ async def async_setup_entry(
if not supported(event): if not supported(event):
continue continue
device_id = get_device_id(event.device) device_id = get_device_id(
event.device, data_bits=entity_info.get(CONF_DATA_BITS)
)
if device_id in device_ids: if device_id in device_ids:
continue continue
device_ids.add(device_id) device_ids.add(device_id)

View File

@ -4,7 +4,7 @@ import logging
from typing import List from typing import List
import boto3 import boto3
from ipify import exceptions, get_ip import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_DOMAIN, CONF_TTL, CONF_ZONE, HTTP_OK from homeassistant.const import CONF_DOMAIN, CONF_TTL, CONF_ZONE, HTTP_OK
@ -84,16 +84,12 @@ def _update_route53(
# Get the IP Address and build an array of changes # Get the IP Address and build an array of changes
try: try:
ipaddress = get_ip() ipaddress = requests.get("https://api.ipify.org/", timeout=5).text
except exceptions.ConnectionError: except requests.RequestException:
_LOGGER.warning("Unable to reach the ipify service") _LOGGER.warning("Unable to reach the ipify service")
return return
except exceptions.ServiceError:
_LOGGER.warning("Unable to complete the ipfy request")
return
changes = [] changes = []
for record in records: for record in records:
_LOGGER.debug("Processing record: %s", record) _LOGGER.debug("Processing record: %s", record)

View File

@ -2,6 +2,6 @@
"domain": "route53", "domain": "route53",
"name": "AWS Route53", "name": "AWS Route53",
"documentation": "https://www.home-assistant.io/integrations/route53", "documentation": "https://www.home-assistant.io/integrations/route53",
"requirements": ["boto3==1.9.252", "ipify==1.0.0"], "requirements": ["boto3==1.9.252"],
"codeowners": [] "codeowners": []
} }

View File

@ -1,6 +1,6 @@
"""Support for SimpliSafe alarm systems.""" """Support for SimpliSafe alarm systems."""
import asyncio import asyncio
import logging from uuid import UUID
from simplipy import API from simplipy import API
from simplipy.errors import InvalidCredentialsError, SimplipyError from simplipy.errors import InvalidCredentialsError, SimplipyError
@ -55,11 +55,10 @@ from .const import (
DATA_CLIENT, DATA_CLIENT,
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
DOMAIN, DOMAIN,
LOGGER,
VOLUMES, VOLUMES,
) )
_LOGGER = logging.getLogger(__name__)
CONF_ACCOUNTS = "accounts" CONF_ACCOUNTS = "accounts"
DATA_LISTENER = "listener" DATA_LISTENER = "listener"
@ -161,6 +160,13 @@ def _async_save_refresh_token(hass, config_entry, token):
) )
async def async_get_client_id(hass):
"""Get a client ID (based on the HASS unique ID) for the SimpliSafe API."""
hass_id = await hass.helpers.instance_id.async_get()
# SimpliSafe requires full, "dashed" versions of UUIDs:
return str(UUID(hass_id))
async def async_register_base_station(hass, system, config_entry_id): async def async_register_base_station(hass, system, config_entry_id):
"""Register a new bridge.""" """Register a new bridge."""
device_registry = await dr.async_get_registry(hass) device_registry = await dr.async_get_registry(hass)
@ -220,17 +226,18 @@ async def async_setup_entry(hass, config_entry):
_verify_domain_control = verify_domain_control(hass, DOMAIN) _verify_domain_control = verify_domain_control(hass, DOMAIN)
client_id = await async_get_client_id(hass)
websession = aiohttp_client.async_get_clientsession(hass) websession = aiohttp_client.async_get_clientsession(hass)
try: try:
api = await API.login_via_token( api = await API.login_via_token(
config_entry.data[CONF_TOKEN], session=websession config_entry.data[CONF_TOKEN], client_id=client_id, session=websession
) )
except InvalidCredentialsError: except InvalidCredentialsError:
_LOGGER.error("Invalid credentials provided") LOGGER.error("Invalid credentials provided")
return False return False
except SimplipyError as err: except SimplipyError as err:
_LOGGER.error("Config entry failed: %s", err) LOGGER.error("Config entry failed: %s", err)
raise ConfigEntryNotReady raise ConfigEntryNotReady
_async_save_refresh_token(hass, config_entry, api.refresh_token) _async_save_refresh_token(hass, config_entry, api.refresh_token)
@ -252,7 +259,7 @@ async def async_setup_entry(hass, config_entry):
"""Decorate.""" """Decorate."""
system_id = int(call.data[ATTR_SYSTEM_ID]) system_id = int(call.data[ATTR_SYSTEM_ID])
if system_id not in simplisafe.systems: if system_id not in simplisafe.systems:
_LOGGER.error("Unknown system ID in service call: %s", system_id) LOGGER.error("Unknown system ID in service call: %s", system_id)
return return
await coro(call) await coro(call)
@ -266,7 +273,7 @@ async def async_setup_entry(hass, config_entry):
"""Decorate.""" """Decorate."""
system = simplisafe.systems[int(call.data[ATTR_SYSTEM_ID])] system = simplisafe.systems[int(call.data[ATTR_SYSTEM_ID])]
if system.version != 3: if system.version != 3:
_LOGGER.error("Service only available on V3 systems") LOGGER.error("Service only available on V3 systems")
return return
await coro(call) await coro(call)
@ -280,7 +287,7 @@ async def async_setup_entry(hass, config_entry):
try: try:
await system.clear_notifications() await system.clear_notifications()
except SimplipyError as err: except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err) LOGGER.error("Error during service call: %s", err)
return return
@verify_system_exists @verify_system_exists
@ -291,7 +298,7 @@ async def async_setup_entry(hass, config_entry):
try: try:
await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE])
except SimplipyError as err: except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err) LOGGER.error("Error during service call: %s", err)
return return
@verify_system_exists @verify_system_exists
@ -302,7 +309,7 @@ async def async_setup_entry(hass, config_entry):
try: try:
await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE])
except SimplipyError as err: except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err) LOGGER.error("Error during service call: %s", err)
return return
@verify_system_exists @verify_system_exists
@ -320,7 +327,7 @@ async def async_setup_entry(hass, config_entry):
} }
) )
except SimplipyError as err: except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err) LOGGER.error("Error during service call: %s", err)
return return
for service, method, schema in [ for service, method, schema in [
@ -373,16 +380,16 @@ class SimpliSafeWebsocket:
@staticmethod @staticmethod
def _on_connect(): def _on_connect():
"""Define a handler to fire when the websocket is connected.""" """Define a handler to fire when the websocket is connected."""
_LOGGER.info("Connected to websocket") LOGGER.info("Connected to websocket")
@staticmethod @staticmethod
def _on_disconnect(): def _on_disconnect():
"""Define a handler to fire when the websocket is disconnected.""" """Define a handler to fire when the websocket is disconnected."""
_LOGGER.info("Disconnected from websocket") LOGGER.info("Disconnected from websocket")
def _on_event(self, event): def _on_event(self, event):
"""Define a handler to fire when a new SimpliSafe event arrives.""" """Define a handler to fire when a new SimpliSafe event arrives."""
_LOGGER.debug("New websocket event: %s", event) LOGGER.debug("New websocket event: %s", event)
async_dispatcher_send( async_dispatcher_send(
self._hass, TOPIC_UPDATE_WEBSOCKET.format(event.system_id), event self._hass, TOPIC_UPDATE_WEBSOCKET.format(event.system_id), event
) )
@ -451,7 +458,7 @@ class SimpliSafe:
if not to_add: if not to_add:
return return
_LOGGER.debug("New system notifications: %s", to_add) LOGGER.debug("New system notifications: %s", to_add)
self._system_notifications[system.system_id].update(to_add) self._system_notifications[system.system_id].update(to_add)
@ -492,7 +499,7 @@ class SimpliSafe:
system.system_id system.system_id
] = await system.get_latest_event() ] = await system.get_latest_event()
except SimplipyError as err: except SimplipyError as err:
_LOGGER.error("Error while fetching initial event: %s", err) LOGGER.error("Error while fetching initial event: %s", err)
self.initial_event_to_use[system.system_id] = {} self.initial_event_to_use[system.system_id] = {}
async def refresh(event_time): async def refresh(event_time):
@ -512,7 +519,7 @@ class SimpliSafe:
"""Update a system.""" """Update a system."""
await system.update() await system.update()
self._async_process_new_notifications(system) self._async_process_new_notifications(system)
_LOGGER.debug('Updated REST API data for "%s"', system.address) LOGGER.debug('Updated REST API data for "%s"', system.address)
async_dispatcher_send( async_dispatcher_send(
self._hass, TOPIC_UPDATE_REST_API.format(system.system_id) self._hass, TOPIC_UPDATE_REST_API.format(system.system_id)
) )
@ -523,27 +530,37 @@ class SimpliSafe:
for result in results: for result in results:
if isinstance(result, InvalidCredentialsError): if isinstance(result, InvalidCredentialsError):
if self._emergency_refresh_token_used: if self._emergency_refresh_token_used:
_LOGGER.error( LOGGER.error(
"SimpliSafe authentication disconnected. Please restart HASS" "Token disconnected or invalid. Please re-auth the "
"SimpliSafe integration in HASS"
) )
remove_listener = self._hass.data[DOMAIN][DATA_LISTENER].pop( self._hass.async_create_task(
self._config_entry.entry_id self._hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "reauth"},
data=self._config_entry.data,
)
) )
remove_listener()
return return
_LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") LOGGER.warning("SimpliSafe cloud error; trying stored refresh token")
self._emergency_refresh_token_used = True self._emergency_refresh_token_used = True
return await self._api.refresh_access_token(
self._config_entry.data[CONF_TOKEN] try:
) await self._api.refresh_access_token(
self._config_entry.data[CONF_TOKEN]
)
return
except SimplipyError as err:
LOGGER.error("Error while using stored refresh token: %s", err)
return
if isinstance(result, SimplipyError): if isinstance(result, SimplipyError):
_LOGGER.error("SimpliSafe error while updating: %s", result) LOGGER.error("SimpliSafe error while updating: %s", result)
return return
if isinstance(result, SimplipyError): if isinstance(result, Exception): # pylint: disable=broad-except
_LOGGER.error("Unknown error while updating: %s", result) LOGGER.error("Unknown error while updating: %s", result)
return return
if self._api.refresh_token != self._config_entry.data[CONF_TOKEN]: if self._api.refresh_token != self._config_entry.data[CONF_TOKEN]:

View File

@ -1,5 +1,4 @@
"""Support for SimpliSafe alarm control panels.""" """Support for SimpliSafe alarm control panels."""
import logging
import re import re
from simplipy.errors import SimplipyError from simplipy.errors import SimplipyError
@ -50,11 +49,10 @@ from .const import (
ATTR_VOICE_PROMPT_VOLUME, ATTR_VOICE_PROMPT_VOLUME,
DATA_CLIENT, DATA_CLIENT,
DOMAIN, DOMAIN,
LOGGER,
VOLUME_STRING_MAP, VOLUME_STRING_MAP,
) )
_LOGGER = logging.getLogger(__name__)
ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level" ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level"
ATTR_GSM_STRENGTH = "gsm_strength" ATTR_GSM_STRENGTH = "gsm_strength"
ATTR_PIN_NAME = "pin_name" ATTR_PIN_NAME = "pin_name"
@ -146,7 +144,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
return True return True
if not code or code != self._simplisafe.options[CONF_CODE]: if not code or code != self._simplisafe.options[CONF_CODE]:
_LOGGER.warning( LOGGER.warning(
"Incorrect alarm code entered (target state: %s): %s", state, code "Incorrect alarm code entered (target state: %s): %s", state, code
) )
return False return False
@ -161,7 +159,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
try: try:
await self._system.set_off() await self._system.set_off()
except SimplipyError as err: except SimplipyError as err:
_LOGGER.error('Error while disarming "%s": %s', self._system.name, err) LOGGER.error('Error while disarming "%s": %s', self._system.name, err)
return return
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
@ -174,7 +172,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
try: try:
await self._system.set_home() await self._system.set_home()
except SimplipyError as err: except SimplipyError as err:
_LOGGER.error('Error while arming "%s" (home): %s', self._system.name, err) LOGGER.error('Error while arming "%s" (home): %s', self._system.name, err)
return return
self._state = STATE_ALARM_ARMED_HOME self._state = STATE_ALARM_ARMED_HOME
@ -187,7 +185,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity):
try: try:
await self._system.set_away() await self._system.set_away()
except SimplipyError as err: except SimplipyError as err:
_LOGGER.error('Error while arming "%s" (away): %s', self._system.name, err) LOGGER.error('Error while arming "%s" (away): %s', self._system.name, err)
return return
self._state = STATE_ALARM_ARMING self._state = STATE_ALARM_ARMING

View File

@ -1,6 +1,10 @@
"""Config flow to configure the SimpliSafe component.""" """Config flow to configure the SimpliSafe component."""
from simplipy import API from simplipy import API
from simplipy.errors import SimplipyError from simplipy.errors import (
InvalidCredentialsError,
PendingAuthorizationError,
SimplipyError,
)
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -8,7 +12,8 @@ from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERN
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from .const import DOMAIN # pylint: disable=unused-import from . import async_get_client_id
from .const import DOMAIN, LOGGER # pylint: disable=unused-import
class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@ -19,21 +24,18 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self): def __init__(self):
"""Initialize the config flow.""" """Initialize the config flow."""
self.data_schema = vol.Schema( self.full_data_schema = vol.Schema(
{ {
vol.Required(CONF_USERNAME): str, vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str, vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_CODE): str, vol.Optional(CONF_CODE): str,
} }
) )
self.password_data_schema = vol.Schema({vol.Required(CONF_PASSWORD): str})
async def _show_form(self, errors=None): self._code = None
"""Show the form to the user.""" self._password = None
return self.async_show_form( self._username = None
step_id="user",
data_schema=self.data_schema,
errors=errors if errors else {},
)
@staticmethod @staticmethod
@callback @callback
@ -41,34 +43,112 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Define the config flow to handle options.""" """Define the config flow to handle options."""
return SimpliSafeOptionsFlowHandler(config_entry) return SimpliSafeOptionsFlowHandler(config_entry)
async def _async_get_simplisafe_api(self):
"""Get an authenticated SimpliSafe API client."""
client_id = await async_get_client_id(self.hass)
websession = aiohttp_client.async_get_clientsession(self.hass)
return await API.login_via_credentials(
self._username, self._password, client_id=client_id, session=websession,
)
async def _async_login_during_step(self, *, step_id, form_schema):
"""Attempt to log into the API from within a config flow step."""
errors = {}
try:
simplisafe = await self._async_get_simplisafe_api()
except PendingAuthorizationError:
LOGGER.info("Awaiting confirmation of MFA email click")
return await self.async_step_mfa()
except InvalidCredentialsError:
errors = {"base": "invalid_credentials"}
except SimplipyError as err:
LOGGER.error("Unknown error while logging into SimpliSafe: %s", err)
errors = {"base": "unknown"}
if errors:
return self.async_show_form(
step_id=step_id, data_schema=form_schema, errors=errors,
)
return await self.async_step_finish(
{
CONF_USERNAME: self._username,
CONF_TOKEN: simplisafe.refresh_token,
CONF_CODE: self._code,
}
)
async def async_step_finish(self, user_input=None):
"""Handle finish config entry setup."""
existing_entry = await self.async_set_unique_id(self._username)
if existing_entry:
self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=self._username, data=user_input)
async def async_step_import(self, import_config): async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml.""" """Import a config entry from configuration.yaml."""
return await self.async_step_user(import_config) return await self.async_step_user(import_config)
async def async_step_mfa(self, user_input=None):
"""Handle multi-factor auth confirmation."""
if user_input is None:
return self.async_show_form(step_id="mfa")
try:
simplisafe = await self._async_get_simplisafe_api()
except PendingAuthorizationError:
LOGGER.error("Still awaiting confirmation of MFA email click")
return self.async_show_form(
step_id="mfa", errors={"base": "still_awaiting_mfa"}
)
return await self.async_step_finish(
{
CONF_USERNAME: self._username,
CONF_TOKEN: simplisafe.refresh_token,
CONF_CODE: self._code,
}
)
async def async_step_reauth(self, config):
"""Handle configuration by re-auth."""
self._code = config.get(CONF_CODE)
self._username = config[CONF_USERNAME]
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None):
"""Handle re-auth completion."""
if not user_input:
return self.async_show_form(
step_id="reauth_confirm", data_schema=self.password_data_schema
)
self._password = user_input[CONF_PASSWORD]
return await self._async_login_during_step(
step_id="reauth_confirm", form_schema=self.password_data_schema
)
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle the start of the config flow.""" """Handle the start of the config flow."""
if not user_input: if not user_input:
return await self._show_form() return self.async_show_form(
step_id="user", data_schema=self.full_data_schema
)
await self.async_set_unique_id(user_input[CONF_USERNAME]) await self.async_set_unique_id(user_input[CONF_USERNAME])
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
websession = aiohttp_client.async_get_clientsession(self.hass) self._code = user_input.get(CONF_CODE)
self._password = user_input[CONF_PASSWORD]
self._username = user_input[CONF_USERNAME]
try: return await self._async_login_during_step(
simplisafe = await API.login_via_credentials( step_id="user", form_schema=self.full_data_schema
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session=websession
)
except SimplipyError:
return await self._show_form(errors={"base": "invalid_credentials"})
return self.async_create_entry(
title=user_input[CONF_USERNAME],
data={
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_TOKEN: simplisafe.refresh_token,
CONF_CODE: user_input.get(CONF_CODE),
},
) )

View File

@ -1,8 +1,11 @@
"""Define constants for the SimpliSafe component.""" """Define constants for the SimpliSafe component."""
from datetime import timedelta from datetime import timedelta
import logging
from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF
LOGGER = logging.getLogger(__package__)
DOMAIN = "simplisafe" DOMAIN = "simplisafe"
DATA_CLIENT = "client" DATA_CLIENT = "client"

View File

@ -1,6 +1,4 @@
"""Support for SimpliSafe locks.""" """Support for SimpliSafe locks."""
import logging
from simplipy.errors import SimplipyError from simplipy.errors import SimplipyError
from simplipy.lock import LockStates from simplipy.lock import LockStates
from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED
@ -9,9 +7,7 @@ from homeassistant.components.lock import LockEntity
from homeassistant.core import callback from homeassistant.core import callback
from . import SimpliSafeEntity from . import SimpliSafeEntity
from .const import DATA_CLIENT, DOMAIN from .const import DATA_CLIENT, DOMAIN, LOGGER
_LOGGER = logging.getLogger(__name__)
ATTR_LOCK_LOW_BATTERY = "lock_low_battery" ATTR_LOCK_LOW_BATTERY = "lock_low_battery"
ATTR_JAMMED = "jammed" ATTR_JAMMED = "jammed"
@ -52,7 +48,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity):
try: try:
await self._lock.lock() await self._lock.lock()
except SimplipyError as err: except SimplipyError as err:
_LOGGER.error('Error while locking "%s": %s', self._lock.name, err) LOGGER.error('Error while locking "%s": %s', self._lock.name, err)
return return
self._is_locked = True self._is_locked = True
@ -62,7 +58,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity):
try: try:
await self._lock.unlock() await self._lock.unlock()
except SimplipyError as err: except SimplipyError as err:
_LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err) LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err)
return return
self._is_locked = False self._is_locked = False

View File

@ -3,6 +3,6 @@
"name": "SimpliSafe", "name": "SimpliSafe",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe", "documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==9.2.0"], "requirements": ["simplisafe-python==9.2.1"],
"codeowners": ["@bachya"] "codeowners": ["@bachya"]
} }

View File

@ -1,6 +1,17 @@
{ {
"config": { "config": {
"step": { "step": {
"mfa": {
"title": "SimpliSafe Multi-Factor Authentication",
"description": "Check your email for a link from SimpliSafe. After verifying the link, return here to complete the installation of the integration."
},
"reauth_confirm": {
"title": "Re-link SimpliSafe Account",
"description": "Your access token has expired or been revoked. Enter your password to re-link your account.",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
},
"user": { "user": {
"title": "Fill in your information.", "title": "Fill in your information.",
"data": { "data": {
@ -12,10 +23,13 @@
}, },
"error": { "error": {
"identifier_exists": "Account already registered", "identifier_exists": "Account already registered",
"invalid_credentials": "Invalid credentials" "invalid_credentials": "Invalid credentials",
"still_awaiting_mfa": "Still awaiting MFA email click",
"unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"abort": { "abort": {
"already_configured": "This SimpliSafe account is already in use." "already_configured": "This SimpliSafe account is already in use.",
"reauth_successful": "SimpliSafe successfully reauthenticated."
} }
}, },
"options": { "options": {

View File

@ -1,18 +1,32 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "This SimpliSafe account is already in use." "already_configured": "This SimpliSafe account is already in use.",
"reauth_successful": "SimpliSafe successfully reauthenticated."
}, },
"error": { "error": {
"identifier_exists": "Account already registered", "identifier_exists": "Account already registered",
"invalid_credentials": "Invalid credentials" "invalid_credentials": "Invalid credentials",
"still_awaiting_mfa": "Still awaiting MFA email click",
"unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"step": { "step": {
"mfa": {
"description": "Check your email for a link from SimpliSafe. After verifying the link, return here to complete the installation of the integration.",
"title": "SimpliSafe Multi-Factor Authentication"
},
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"description": "Your access token has expired or been revoked. Enter your password to re-link your account.",
"title": "Re-link SimpliSafe Account"
},
"user": { "user": {
"data": { "data": {
"code": "Code (used in Home Assistant UI)", "code": "Code (used in Home Assistant UI)",
"password": "Password", "password": "[%key:common::config_flow::data::password%]",
"username": "Email" "username": "[%key:common::config_flow::data::email%]"
}, },
"title": "Fill in your information." "title": "Fill in your information."
} }

View File

@ -3,7 +3,7 @@
"name": "SmartThings", "name": "SmartThings",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/smartthings", "documentation": "https://www.home-assistant.io/integrations/smartthings",
"requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.1"], "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.2"],
"dependencies": ["webhook"], "dependencies": ["webhook"],
"after_dependencies": ["cloud"], "after_dependencies": ["cloud"],
"codeowners": ["@andrewsayre"] "codeowners": ["@andrewsayre"]

View File

@ -224,7 +224,7 @@ SENSOR_ENTITIES = {
"power_meter_reading_low": { "power_meter_reading_low": {
ATTR_NAME: "Electricity Meter Feed IN Tariff 2", ATTR_NAME: "Electricity Meter Feed IN Tariff 2",
ATTR_SECTION: "power_usage", ATTR_SECTION: "power_usage",
ATTR_MEASUREMENT: "meter_high", ATTR_MEASUREMENT: "meter_low",
ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR,
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:power-plug", ATTR_ICON: "mdi:power-plug",

View File

@ -2,6 +2,6 @@
"domain": "xbox_live", "domain": "xbox_live",
"name": "Xbox Live", "name": "Xbox Live",
"documentation": "https://www.home-assistant.io/integrations/xbox_live", "documentation": "https://www.home-assistant.io/integrations/xbox_live",
"requirements": ["xboxapi==0.1.1"], "requirements": ["xboxapi==2.0.0"],
"codeowners": ["@MartinHjelmare"] "codeowners": ["@MartinHjelmare"]
} }

View File

@ -3,7 +3,7 @@ from datetime import timedelta
import logging import logging
import voluptuous as vol import voluptuous as vol
from xboxapi import xbox_api from xboxapi import Client
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_API_KEY, CONF_SCAN_INTERVAL from homeassistant.const import CONF_API_KEY, CONF_SCAN_INTERVAL
@ -28,17 +28,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Xbox platform.""" """Set up the Xbox platform."""
api = xbox_api.XboxApi(config[CONF_API_KEY]) api = Client(api_key=config[CONF_API_KEY])
entities = [] entities = []
# request personal profile to check api connection # request profile info to check api connection
profile = api.get_profile() response = api.api_get("profile")
if profile.get("error_code") is not None: if not response.ok:
_LOGGER.error( _LOGGER.error(
"Can't setup XboxAPI connection. Check your account or " "Can't setup X API connection. Check your account or "
"api key on xboxapi.com. Code: %s Description: %s ", "api key on xapi.us. Code: %s Description: %s ",
profile.get("error_code", "unknown"), response.status_code,
profile.get("error_message", "unknown"), response.reason,
) )
return return
@ -59,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def get_user_gamercard(api, xuid): def get_user_gamercard(api, xuid):
"""Get profile info.""" """Get profile info."""
gamercard = api.get_user_gamercard(xuid) gamercard = api.gamer(gamertag="", xuid=xuid).get("gamercard")
_LOGGER.debug("User gamercard: %s", gamercard) _LOGGER.debug("User gamercard: %s", gamercard)
if gamercard.get("success", True) and gamercard.get("code") is None: if gamercard.get("success", True) and gamercard.get("code") is None:
@ -82,11 +82,11 @@ class XboxSensor(Entity):
self._presence = [] self._presence = []
self._xuid = xuid self._xuid = xuid
self._api = api self._api = api
self._gamertag = gamercard.get("gamertag") self._gamertag = gamercard["gamertag"]
self._gamerscore = gamercard.get("gamerscore") self._gamerscore = gamercard["gamerscore"]
self._interval = interval self._interval = interval
self._picture = gamercard.get("gamerpicSmallSslImagePath") self._picture = gamercard["gamerpicSmallSslImagePath"]
self._tier = gamercard.get("tier") self._tier = gamercard["tier"]
@property @property
def name(self): def name(self):
@ -111,10 +111,8 @@ class XboxSensor(Entity):
attributes["tier"] = self._tier attributes["tier"] = self._tier
for device in self._presence: for device in self._presence:
for title in device.get("titles"): for title in device["titles"]:
attributes[ attributes[f'{device["type"]} {title["placement"]}'] = title["name"]
f'{device.get("type")} {title.get("placement")}'
] = title.get("name")
return attributes return attributes
@ -140,7 +138,7 @@ class XboxSensor(Entity):
def update(self): def update(self):
"""Update state data from Xbox API.""" """Update state data from Xbox API."""
presence = self._api.get_user_presence(self._xuid) presence = self._api.gamer(gamertag="", xuid=self._xuid).get("presence")
_LOGGER.debug("User presence: %s", presence) _LOGGER.debug("User presence: %s", presence)
self._state = presence.get("state") self._state = presence["state"]
self._presence = presence.get("devices", []) self._presence = presence.get("devices", [])

View File

@ -209,6 +209,11 @@ def setup(hass, config):
return return
info = info_from_service(service_info) info = info_from_service(service_info)
if not info:
# Prevent the browser thread from collapsing
_LOGGER.debug("Failed to get addresses for device %s", name)
return
_LOGGER.debug("Discovered new device %s %s", name, info) _LOGGER.debug("Discovered new device %s %s", name, info)
# If we can handle it as a HomeKit discovery, we do that here. # If we can handle it as a HomeKit discovery, we do that here.
@ -310,6 +315,9 @@ def info_from_service(service):
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
if not service.addresses:
return None
address = service.addresses[0] address = service.addresses[0]
info = { info = {

View File

@ -1,7 +1,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 113 MINOR_VERSION = 113
PATCH_VERSION = "0" PATCH_VERSION = "1"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 1) REQUIRED_PYTHON_VER = (3, 7, 1)

View File

@ -23,6 +23,7 @@ from typing import (
Callable, Callable,
Coroutine, Coroutine,
Dict, Dict,
Iterable,
List, List,
Mapping, Mapping,
Optional, Optional,
@ -98,6 +99,9 @@ CORE_STORAGE_VERSION = 1
DOMAIN = "homeassistant" DOMAIN = "homeassistant"
# How long to wait to log tasks that are blocking
BLOCK_LOG_TIMEOUT = 60
# How long we wait for the result of a service call # How long we wait for the result of a service call
SERVICE_CALL_LIMIT = 10 # seconds SERVICE_CALL_LIMIT = 10 # seconds
@ -393,10 +397,21 @@ class HomeAssistant:
pending = [task for task in self._pending_tasks if not task.done()] pending = [task for task in self._pending_tasks if not task.done()]
self._pending_tasks.clear() self._pending_tasks.clear()
if pending: if pending:
await asyncio.wait(pending) await self._await_and_log_pending(pending)
else: else:
await asyncio.sleep(0) await asyncio.sleep(0)
async def _await_and_log_pending(self, pending: Iterable[Awaitable[Any]]) -> None:
"""Await and log tasks that take a long time."""
wait_time = 0
while pending:
_, pending = await asyncio.wait(pending, timeout=BLOCK_LOG_TIMEOUT)
if not pending:
return
wait_time += BLOCK_LOG_TIMEOUT
for task in pending:
_LOGGER.debug("Waited %s seconds for task: %s", wait_time, task)
def stop(self) -> None: def stop(self) -> None:
"""Stop Home Assistant and shuts down all threads.""" """Stop Home Assistant and shuts down all threads."""
if self.state == CoreState.not_running: # just ignore if self.state == CoreState.not_running: # just ignore

View File

@ -140,7 +140,7 @@ class _ScriptRun:
) -> None: ) -> None:
self._hass = hass self._hass = hass
self._script = script self._script = script
self._variables = variables self._variables = variables or {}
self._context = context self._context = context
self._log_exceptions = log_exceptions self._log_exceptions = log_exceptions
self._step = -1 self._step = -1
@ -431,22 +431,23 @@ class _ScriptRun:
async def _async_repeat_step(self): async def _async_repeat_step(self):
"""Repeat a sequence.""" """Repeat a sequence."""
description = self._action.get(CONF_ALIAS, "sequence") description = self._action.get(CONF_ALIAS, "sequence")
repeat = self._action[CONF_REPEAT] repeat = self._action[CONF_REPEAT]
async def async_run_sequence(iteration, extra_msg="", extra_vars=None): saved_repeat_vars = self._variables.get("repeat")
def set_repeat_var(iteration, count=None):
repeat_vars = {"first": iteration == 1, "index": iteration}
if count:
repeat_vars["last"] = iteration == count
self._variables["repeat"] = repeat_vars
# pylint: disable=protected-access
script = self._script._get_repeat_script(self._step)
async def async_run_sequence(iteration, extra_msg=""):
self._log("Repeating %s: Iteration %i%s", description, iteration, extra_msg) self._log("Repeating %s: Iteration %i%s", description, iteration, extra_msg)
repeat_vars = {"repeat": {"first": iteration == 1, "index": iteration}} await self._async_run_script(script)
if extra_vars:
repeat_vars["repeat"].update(extra_vars)
# pylint: disable=protected-access
await self._async_run_script(
self._script._get_repeat_script(self._step),
# Add repeat to variables. Override if it already exists in case of
# nested calls.
{**(self._variables or {}), **repeat_vars},
)
if CONF_COUNT in repeat: if CONF_COUNT in repeat:
count = repeat[CONF_COUNT] count = repeat[CONF_COUNT]
@ -461,10 +462,10 @@ class _ScriptRun:
level=logging.ERROR, level=logging.ERROR,
) )
raise _StopScript raise _StopScript
extra_msg = f" of {count}"
for iteration in range(1, count + 1): for iteration in range(1, count + 1):
await async_run_sequence( set_repeat_var(iteration, count)
iteration, f" of {count}", {"last": iteration == count} await async_run_sequence(iteration, extra_msg)
)
if self._stop.is_set(): if self._stop.is_set():
break break
@ -473,6 +474,7 @@ class _ScriptRun:
await self._async_get_condition(config) for config in repeat[CONF_WHILE] await self._async_get_condition(config) for config in repeat[CONF_WHILE]
] ]
for iteration in itertools.count(1): for iteration in itertools.count(1):
set_repeat_var(iteration)
if self._stop.is_set() or not all( if self._stop.is_set() or not all(
cond(self._hass, self._variables) for cond in conditions cond(self._hass, self._variables) for cond in conditions
): ):
@ -484,12 +486,18 @@ class _ScriptRun:
await self._async_get_condition(config) for config in repeat[CONF_UNTIL] await self._async_get_condition(config) for config in repeat[CONF_UNTIL]
] ]
for iteration in itertools.count(1): for iteration in itertools.count(1):
set_repeat_var(iteration)
await async_run_sequence(iteration) await async_run_sequence(iteration)
if self._stop.is_set() or all( if self._stop.is_set() or all(
cond(self._hass, self._variables) for cond in conditions cond(self._hass, self._variables) for cond in conditions
): ):
break break
if saved_repeat_vars:
self._variables["repeat"] = saved_repeat_vars
else:
del self._variables["repeat"]
async def _async_choose_step(self): async def _async_choose_step(self):
"""Choose a sequence.""" """Choose a sequence."""
# pylint: disable=protected-access # pylint: disable=protected-access
@ -503,11 +511,11 @@ class _ScriptRun:
if choose_data["default"]: if choose_data["default"]:
await self._async_run_script(choose_data["default"]) await self._async_run_script(choose_data["default"])
async def _async_run_script(self, script, variables=None): async def _async_run_script(self, script):
"""Execute a script.""" """Execute a script."""
await self._async_run_long_action( await self._async_run_long_action(
self._hass.async_create_task( self._hass.async_create_task(
script.async_run(variables or self._variables, self._context) script.async_run(self._variables, self._context)
) )
) )

View File

@ -231,7 +231,7 @@ ambiclimate==0.2.1
amcrest==1.7.0 amcrest==1.7.0
# homeassistant.components.androidtv # homeassistant.components.androidtv
androidtv[async]==0.0.45 androidtv[async]==0.0.46
# homeassistant.components.anel_pwrctrl # homeassistant.components.anel_pwrctrl
anel_pwrctrl-homeassistant==0.0.1.dev2 anel_pwrctrl-homeassistant==0.0.1.dev2
@ -473,7 +473,7 @@ directv==0.3.0
discogs_client==2.2.2 discogs_client==2.2.2
# homeassistant.components.discord # homeassistant.components.discord
discord.py==1.3.3 discord.py==1.3.4
# homeassistant.components.updater # homeassistant.components.updater
distro==1.5.0 distro==1.5.0
@ -783,9 +783,6 @@ influxdb==5.2.3
# homeassistant.components.iperf3 # homeassistant.components.iperf3
iperf3==0.1.11 iperf3==0.1.11
# homeassistant.components.route53
ipify==1.0.0
# homeassistant.components.rest # homeassistant.components.rest
# homeassistant.components.verisure # homeassistant.components.verisure
jsonpath==0.82 jsonpath==0.82
@ -1491,7 +1488,7 @@ pynetgear==0.6.1
pynetio==0.1.9.1 pynetio==0.1.9.1
# homeassistant.components.nuki # homeassistant.components.nuki
pynuki==1.3.7 pynuki==1.3.8
# homeassistant.components.nut # homeassistant.components.nut
pynut2==2.1.2 pynut2==2.1.2
@ -1608,7 +1605,7 @@ pysmappee==0.1.5
pysmartapp==0.3.2 pysmartapp==0.3.2
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==0.7.1 pysmartthings==0.7.2
# homeassistant.components.smarty # homeassistant.components.smarty
pysmarty==0.8 pysmarty==0.8
@ -1942,7 +1939,7 @@ simplehound==0.3
simplepush==1.1.4 simplepush==1.1.4
# homeassistant.components.simplisafe # homeassistant.components.simplisafe
simplisafe-python==9.2.0 simplisafe-python==9.2.1
# homeassistant.components.sisyphus # homeassistant.components.sisyphus
sisyphus-control==2.2.1 sisyphus-control==2.2.1
@ -2207,7 +2204,7 @@ wled==0.4.3
xbee-helper==0.0.7 xbee-helper==0.0.7
# homeassistant.components.xbox_live # homeassistant.components.xbox_live
xboxapi==0.1.1 xboxapi==2.0.0
# homeassistant.components.xfinity # homeassistant.components.xfinity
xfinity-gateway==0.0.4 xfinity-gateway==0.0.4

View File

@ -132,7 +132,7 @@ airly==0.0.2
ambiclimate==0.2.1 ambiclimate==0.2.1
# homeassistant.components.androidtv # homeassistant.components.androidtv
androidtv[async]==0.0.45 androidtv[async]==0.0.46
# homeassistant.components.apns # homeassistant.components.apns
apns2==0.3.0 apns2==0.3.0
@ -737,7 +737,7 @@ pysmappee==0.1.5
pysmartapp==0.3.2 pysmartapp==0.3.2
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==0.7.1 pysmartthings==0.7.2
# homeassistant.components.soma # homeassistant.components.soma
pysoma==0.0.10 pysoma==0.0.10
@ -857,7 +857,7 @@ sentry-sdk==0.13.5
simplehound==0.3 simplehound==0.3
# homeassistant.components.simplisafe # homeassistant.components.simplisafe
simplisafe-python==9.2.0 simplisafe-python==9.2.1
# homeassistant.components.sleepiq # homeassistant.components.sleepiq
sleepyq==0.7 sleepyq==0.7

View File

@ -1,14 +1,16 @@
"""Define tests for the SimpliSafe config flow.""" """Define tests for the SimpliSafe config flow."""
import json from simplipy.errors import (
InvalidCredentialsError,
from simplipy.errors import SimplipyError PendingAuthorizationError,
SimplipyError,
)
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.components.simplisafe import DOMAIN from homeassistant.components.simplisafe import DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from tests.async_mock import MagicMock, PropertyMock, mock_open, patch from tests.async_mock import MagicMock, PropertyMock, patch
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -21,11 +23,17 @@ def mock_api():
async def test_duplicate_error(hass): async def test_duplicate_error(hass):
"""Test that errors are shown when duplicates are added.""" """Test that errors are shown when duplicates are added."""
conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} conf = {
CONF_USERNAME: "user@email.com",
CONF_PASSWORD: "password",
CONF_CODE: "1234",
}
MockConfigEntry(domain=DOMAIN, unique_id="user@email.com", data=conf).add_to_hass( MockConfigEntry(
hass domain=DOMAIN,
) unique_id="user@email.com",
data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"},
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf DOMAIN, context={"source": SOURCE_USER}, data=conf
@ -40,7 +48,7 @@ async def test_invalid_credentials(hass):
conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
with patch( with patch(
"simplipy.API.login_via_credentials", side_effect=SimplipyError, "simplipy.API.login_via_credentials", side_effect=InvalidCredentialsError,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf DOMAIN, context={"source": SOURCE_USER}, data=conf
@ -75,15 +83,12 @@ async def test_options_flow(hass):
async def test_show_form(hass): async def test_show_form(hass):
"""Test that the form is served with no input.""" """Test that the form is served with no input."""
with patch( result = await hass.config_entries.flow.async_init(
"homeassistant.components.simplisafe.async_setup_entry", return_value=True DOMAIN, context={"source": SOURCE_IMPORT}
): )
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
async def test_step_import(hass): async def test_step_import(hass):
@ -94,17 +99,9 @@ async def test_step_import(hass):
CONF_CODE: "1234", CONF_CODE: "1234",
} }
mop = mock_open(read_data=json.dumps({"refresh_token": "12345"}))
with patch( with patch(
"homeassistant.components.simplisafe.async_setup_entry", return_value=True "homeassistant.components.simplisafe.async_setup_entry", return_value=True
), patch("simplipy.API.login_via_credentials", return_value=mock_api()), patch( ), patch("simplipy.API.login_via_credentials", return_value=mock_api()):
"homeassistant.util.json.open", mop, create=True
), patch(
"homeassistant.util.json.os.open", return_value=0
), patch(
"homeassistant.util.json.os.replace"
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf DOMAIN, context={"source": SOURCE_USER}, data=conf
) )
@ -118,25 +115,48 @@ async def test_step_import(hass):
} }
async def test_step_reauth(hass):
"""Test that the reauth step works."""
MockConfigEntry(
domain=DOMAIN,
unique_id="user@email.com",
data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"},
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "reauth"},
data={CONF_CODE: "1234", CONF_USERNAME: "user@email.com"},
)
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth_confirm"
with patch(
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
), patch("simplipy.API.login_via_credentials", return_value=mock_api()):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PASSWORD: "password"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "reauth_successful"
assert len(hass.config_entries.async_entries()) == 1
async def test_step_user(hass): async def test_step_user(hass):
"""Test that the user step works.""" """Test that the user step works (without MFA)."""
conf = { conf = {
CONF_USERNAME: "user@email.com", CONF_USERNAME: "user@email.com",
CONF_PASSWORD: "password", CONF_PASSWORD: "password",
CONF_CODE: "1234", CONF_CODE: "1234",
} }
mop = mock_open(read_data=json.dumps({"refresh_token": "12345"}))
with patch( with patch(
"homeassistant.components.simplisafe.async_setup_entry", return_value=True "homeassistant.components.simplisafe.async_setup_entry", return_value=True
), patch("simplipy.API.login_via_credentials", return_value=mock_api()), patch( ), patch("simplipy.API.login_via_credentials", return_value=mock_api()):
"homeassistant.util.json.open", mop, create=True
), patch(
"homeassistant.util.json.os.open", return_value=0
), patch(
"homeassistant.util.json.os.replace"
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf DOMAIN, context={"source": SOURCE_USER}, data=conf
) )
@ -148,3 +168,58 @@ async def test_step_user(hass):
CONF_TOKEN: "12345abc", CONF_TOKEN: "12345abc",
CONF_CODE: "1234", CONF_CODE: "1234",
} }
async def test_step_user_mfa(hass):
"""Test that the user step works when MFA is in the middle."""
conf = {
CONF_USERNAME: "user@email.com",
CONF_PASSWORD: "password",
CONF_CODE: "1234",
}
with patch(
"simplipy.API.login_via_credentials", side_effect=PendingAuthorizationError
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
)
assert result["step_id"] == "mfa"
with patch(
"simplipy.API.login_via_credentials", side_effect=PendingAuthorizationError
):
# Simulate the user pressing the MFA submit button without having clicked
# the link in the MFA email:
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
assert result["step_id"] == "mfa"
with patch(
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
), patch("simplipy.API.login_via_credentials", return_value=mock_api()):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "user@email.com"
assert result["data"] == {
CONF_USERNAME: "user@email.com",
CONF_TOKEN: "12345abc",
CONF_CODE: "1234",
}
async def test_unknown_error(hass):
"""Test that an unknown error raises the correct error."""
conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
with patch(
"simplipy.API.login_via_credentials", side_effect=SimplipyError,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf
)
assert result["errors"] == {"base": "unknown"}

View File

@ -249,7 +249,7 @@ def subscription_factory_fixture():
def device_factory_fixture(): def device_factory_fixture():
"""Fixture for creating mock devices.""" """Fixture for creating mock devices."""
api = Mock(Api) api = Mock(Api)
api.post_device_command.return_value = {} api.post_device_command.return_value = {"results": [{"status": "ACCEPTED"}]}
def _factory(label, capabilities, status: dict = None): def _factory(label, capabilities, status: dict = None):
device_data = { device_data = {

View File

@ -49,6 +49,20 @@ def get_service_info_mock(service_type, name):
) )
def get_service_info_mock_without_an_address(service_type, name):
"""Return service info for get_service_info without any addresses."""
return ServiceInfo(
service_type,
name,
addresses=[],
port=80,
weight=0,
priority=0,
server="name.local.",
properties=PROPERTIES,
)
def get_homekit_info_mock(model, pairing_status): def get_homekit_info_mock(model, pairing_status):
"""Return homekit info for get_service_info for an homekit device.""" """Return homekit info for get_service_info for an homekit device."""
@ -286,6 +300,15 @@ async def test_info_from_service_non_utf8(hass):
assert raw_info["non-utf8-value"] is NON_UTF8_VALUE assert raw_info["non-utf8-value"] is NON_UTF8_VALUE
async def test_info_from_service_with_addresses(hass):
"""Test info_from_service does not throw when there are no addresses."""
service_type = "_test._tcp.local."
info = zeroconf.info_from_service(
get_service_info_mock_without_an_address(service_type, f"test.{service_type}")
)
assert info is None
async def test_get_instance(hass, mock_zeroconf): async def test_get_instance(hass, mock_zeroconf):
"""Test we get an instance.""" """Test we get an instance."""
assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf assert await hass.components.zeroconf.async_get_instance() is mock_zeroconf

View File

@ -188,8 +188,8 @@ async def test_platform_warn_slow_setup(hass):
assert mock_call.called assert mock_call.called
# mock_calls[0] is the warning message for component setup # mock_calls[0] is the warning message for component setup
# mock_calls[5] is the warning message for platform setup # mock_calls[6] is the warning message for platform setup
timeout, logger_method = mock_call.mock_calls[5][1][:2] timeout, logger_method = mock_call.mock_calls[6][1][:2]
assert timeout == entity_platform.SLOW_SETUP_WARNING assert timeout == entity_platform.SLOW_SETUP_WARNING
assert logger_method == _LOGGER.warning assert logger_method == _LOGGER.warning

View File

@ -854,6 +854,122 @@ async def test_repeat_conditional(hass, condition):
assert event.data.get("index") == str(index + 1) assert event.data.get("index") == str(index + 1)
@pytest.mark.parametrize("condition", ["while", "until"])
async def test_repeat_var_in_condition(hass, condition):
"""Test repeat action w/ while option."""
event = "test_event"
events = async_capture_events(hass, event)
sequence = {"repeat": {"sequence": {"event": event}}}
if condition == "while":
sequence["repeat"]["while"] = {
"condition": "template",
"value_template": "{{ repeat.index <= 2 }}",
}
else:
sequence["repeat"]["until"] = {
"condition": "template",
"value_template": "{{ repeat.index == 2 }}",
}
script_obj = script.Script(hass, cv.SCRIPT_SCHEMA(sequence))
with mock.patch(
"homeassistant.helpers.condition._LOGGER.error",
side_effect=AssertionError("Template Error"),
):
await script_obj.async_run()
assert len(events) == 2
async def test_repeat_nested(hass):
"""Test nested repeats."""
event = "test_event"
events = async_capture_events(hass, event)
sequence = cv.SCRIPT_SCHEMA(
[
{
"event": event,
"event_data_template": {
"repeat": "{{ None if repeat is not defined else repeat }}"
},
},
{
"repeat": {
"count": 2,
"sequence": [
{
"event": event,
"event_data_template": {
"first": "{{ repeat.first }}",
"index": "{{ repeat.index }}",
"last": "{{ repeat.last }}",
},
},
{
"repeat": {
"count": 2,
"sequence": {
"event": event,
"event_data_template": {
"first": "{{ repeat.first }}",
"index": "{{ repeat.index }}",
"last": "{{ repeat.last }}",
},
},
}
},
{
"event": event,
"event_data_template": {
"first": "{{ repeat.first }}",
"index": "{{ repeat.index }}",
"last": "{{ repeat.last }}",
},
},
],
}
},
{
"event": event,
"event_data_template": {
"repeat": "{{ None if repeat is not defined else repeat }}"
},
},
]
)
script_obj = script.Script(hass, sequence, "test script")
with mock.patch(
"homeassistant.helpers.condition._LOGGER.error",
side_effect=AssertionError("Template Error"),
):
await script_obj.async_run()
assert len(events) == 10
assert events[0].data == {"repeat": "None"}
assert events[-1].data == {"repeat": "None"}
for index, result in enumerate(
(
("True", "1", "False"),
("True", "1", "False"),
("False", "2", "True"),
("True", "1", "False"),
("False", "2", "True"),
("True", "1", "False"),
("False", "2", "True"),
("False", "2", "True"),
),
1,
):
assert events[index].data == {
"first": result[0],
"index": result[1],
"last": result[2],
}
@pytest.mark.parametrize("var,result", [(1, "first"), (2, "second"), (3, "default")]) @pytest.mark.parametrize("var,result", [(1, "first"), (2, "second"), (3, "default")])
async def test_choose(hass, var, result): async def test_choose(hass, var, result):
"""Test choose action.""" """Test choose action."""

View File

@ -1393,3 +1393,24 @@ async def test_start_events(hass):
EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STARTED,
] ]
assert core_states == [ha.CoreState.starting, ha.CoreState.running] assert core_states == [ha.CoreState.starting, ha.CoreState.running]
async def test_log_blocking_events(hass, caplog):
"""Ensure we log which task is blocking startup when debug logging is on."""
caplog.set_level(logging.DEBUG)
async def _wait_a_bit_1():
await asyncio.sleep(0.1)
async def _wait_a_bit_2():
await asyncio.sleep(0.1)
hass.async_create_task(_wait_a_bit_1())
await hass.async_block_till_done()
with patch.object(ha, "BLOCK_LOG_TIMEOUT", 0.00001):
hass.async_create_task(_wait_a_bit_2())
await hass.async_block_till_done()
assert "_wait_a_bit_2" in caplog.text
assert "_wait_a_bit_1" not in caplog.text