Merge pull request #26345 from home-assistant/rc

0.98.2
This commit is contained in:
Paulus Schoutsen 2019-09-01 23:18:45 -07:00 committed by GitHub
commit 8ae5ece6bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 195 additions and 27 deletions

View File

@ -56,6 +56,11 @@ class Auth:
return await self._async_request_new_token(lwa_params)
@callback
def async_invalidate_access_token(self):
"""Invalidate access token."""
self._prefs[STORAGE_ACCESS_TOKEN] = None
async def async_get_access_token(self):
"""Perform access token or token refresh request."""
async with self._get_token_lock:

View File

@ -11,6 +11,7 @@ from homeassistant.const import (
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNLOCKED,
STATE_UNKNOWN,
)
import homeassistant.components.climate.const as climate
from homeassistant.components import light, fan, cover
@ -443,7 +444,17 @@ class AlexaTemperatureSensor(AlexaCapibility):
if self.entity.domain == climate.DOMAIN:
unit = self.hass.config.units.temperature_unit
temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)
return {"value": float(temp), "scale": API_TEMP_UNITS[unit]}
if temp in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return None
try:
temp = float(temp)
except ValueError:
_LOGGER.warning("Invalid temp value %s for %s", temp, self.entity.entity_id)
return None
return {"value": temp, "scale": API_TEMP_UNITS[unit]}
class AlexaContactSensor(AlexaCapibility):
@ -591,4 +602,12 @@ class AlexaThermostatController(AlexaCapibility):
if temp is None:
return None
return {"value": float(temp), "scale": API_TEMP_UNITS[unit]}
try:
temp = float(temp)
except ValueError:
_LOGGER.warning(
"Invalid temp value %s for %s in %s", temp, name, self.entity.entity_id
)
return None
return {"value": temp, "scale": API_TEMP_UNITS[unit]}

View File

@ -1,4 +1,6 @@
"""Config helpers for Alexa."""
from homeassistant.core import callback
from .state_report import async_enable_proactive_mode
@ -55,11 +57,17 @@ class AbstractConfig:
unsub_func()
self._unsub_proactive_report = None
@callback
def should_expose(self, entity_id):
"""If an entity should be exposed."""
# pylint: disable=no-self-use
return False
@callback
def async_invalidate_access_token(self):
"""Invalidate access token."""
raise NotImplementedError
async def async_get_access_token(self):
"""Get an access token."""
raise NotImplementedError

View File

@ -57,6 +57,11 @@ class AlexaConfig(AbstractConfig):
"""If an entity should be exposed."""
return self._config[CONF_FILTER](entity_id)
@core.callback
def async_invalidate_access_token(self):
"""Invalidate access token."""
self._auth.async_invalidate_access_token()
async def async_get_access_token(self):
"""Get an access token."""
return await self._auth.async_get_access_token()

View File

@ -51,7 +51,9 @@ async def async_enable_proactive_mode(hass, smart_home_config):
)
async def async_send_changereport_message(hass, config, alexa_entity):
async def async_send_changereport_message(
hass, config, alexa_entity, *, invalidate_access_token=True
):
"""Send a ChangeReport message for an Alexa entity.
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-with-changereport-events
@ -88,21 +90,30 @@ async def async_send_changereport_message(hass, config, alexa_entity):
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Timeout sending report to Alexa.")
return None
return
response_text = await response.text()
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
_LOGGER.debug("Received (%s): %s", response.status, response_text)
if response.status != 202:
response_json = json.loads(response_text)
_LOGGER.error(
"Error when sending ChangeReport to Alexa: %s: %s",
response_json["payload"]["code"],
response_json["payload"]["description"],
if response.status == 202 and not invalidate_access_token:
return
response_json = json.loads(response_text)
if response_json["payload"]["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION":
config.async_invalidate_access_token()
return await async_send_changereport_message(
hass, config, alexa_entity, invalidate_access_token=False
)
_LOGGER.error(
"Error when sending ChangeReport to Alexa: %s: %s",
response_json["payload"]["code"],
response_json["payload"]["description"],
)
async def async_send_add_or_update_message(hass, config, entity_ids):
"""Send an AddOrUpdateReport message for entities.

View File

@ -7,6 +7,7 @@ import aiohttp
import async_timeout
from hass_nabucasa import cloud_api
from homeassistant.core import callback
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
from homeassistant.helpers import entity_registry
from homeassistant.helpers.event import async_call_later
@ -95,9 +96,14 @@ class AlexaConfig(alexa_config.AbstractConfig):
entity_config = entity_configs.get(entity_id, {})
return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE)
@callback
def async_invalidate_access_token(self):
"""Invalidate access token."""
self._token_valid = None
async def async_get_access_token(self):
"""Get an access token."""
if self._token_valid is not None and self._token_valid < utcnow():
if self._token_valid is not None and self._token_valid > utcnow():
return self._token
resp = await cloud_api.async_alexa_access_token(self._cloud)

View File

@ -157,8 +157,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
async def _update_entry(self, entry, host):
"""Update existing entry."""
if entry.data[CONF_HOST] == host:
return self.async_abort(reason="already_configured")
entry.data[CONF_HOST] = host
self.hass.config_entries.async_update_entry(entry)
return self.async_abort(reason="updated_instance")
async def async_step_ssdp(self, discovery_info):
"""Handle a discovered deCONZ bridge."""
@ -175,8 +179,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
if uuid in gateways:
entry = gateways[uuid].config_entry
await self._update_entry(entry, discovery_info[CONF_HOST])
return self.async_abort(reason="updated_instance")
return await self._update_entry(entry, discovery_info[CONF_HOST])
bridgeid = discovery_info[ATTR_SERIAL]
if any(
@ -224,8 +227,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
if bridgeid in gateway_entries:
entry = gateway_entries[bridgeid]
await self._update_entry(entry, user_input[CONF_HOST])
return self.async_abort(reason="updated_instance")
return await self._update_entry(entry, user_input[CONF_HOST])
self._hassio_discovery = user_input

View File

@ -34,13 +34,13 @@ class DemoFan(FanEntity):
self._supported_features = supported_features
self._speed = STATE_OFF
self.oscillating = None
self.direction = None
self._direction = None
self._name = name
if supported_features & SUPPORT_OSCILLATE:
self.oscillating = False
if supported_features & SUPPORT_DIRECTION:
self.direction = "forward"
self._direction = "forward"
@property
def name(self) -> str:
@ -80,7 +80,7 @@ class DemoFan(FanEntity):
def set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
self.direction = direction
self._direction = direction
self.schedule_update_ha_state()
def oscillate(self, oscillating: bool) -> None:
@ -91,7 +91,7 @@ class DemoFan(FanEntity):
@property
def current_direction(self) -> str:
"""Fan direction."""
return self.direction
return self._direction
@property
def supported_features(self) -> int:

View File

@ -54,7 +54,7 @@ PROP_TO_ATTR = {
"speed": ATTR_SPEED,
"speed_list": ATTR_SPEED_LIST,
"oscillating": ATTR_OSCILLATING,
"direction": ATTR_DIRECTION,
"current_direction": ATTR_DIRECTION,
} # type: dict
FAN_SET_SPEED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend(

View File

@ -52,7 +52,7 @@ class GoogleMapsScanner:
self.see = see
self.username = config[CONF_USERNAME]
self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY]
self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(60)
self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=60)
credfile = "{}.{}".format(
hass.config.path(CREDENTIALS_FILE), slugify(self.username)

View File

@ -243,7 +243,7 @@ class TemplateFan(FanEntity):
return self._oscillating
@property
def direction(self):
def current_direction(self):
"""Return the oscillation state."""
return self._direction

View File

@ -3,7 +3,7 @@
"name": "Tuya",
"documentation": "https://www.home-assistant.io/components/tuya",
"requirements": [
"tuyaha==0.0.3"
"tuyaha==0.0.4"
],
"dependencies": [],
"codeowners": []

View File

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

View File

@ -1857,7 +1857,7 @@ tplink==0.2.1
transmissionrpc==0.11
# homeassistant.components.tuya
tuyaha==0.0.3
tuyaha==0.0.4
# homeassistant.components.twentemilieu
twentemilieu==0.1.0

View File

@ -171,6 +171,12 @@ class ReportedProperties:
"""Initialize class."""
self.properties = properties
def assert_not_has_property(self, namespace, name):
"""Assert a property does not exist."""
for prop in self.properties:
if prop["namespace"] == namespace and prop["name"] == name:
assert False, "Property %s:%s exists"
def assert_equal(self, namespace, name, value):
"""Assert a property is equal to a given value."""
for prop in self.properties:

View File

@ -1,7 +1,15 @@
"""Test Alexa capabilities."""
import pytest
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS,
STATE_LOCKED,
STATE_UNLOCKED,
STATE_UNKNOWN,
STATE_UNAVAILABLE,
)
from homeassistant.components import climate
from homeassistant.components.alexa import smart_home
from tests.common import async_mock_service
@ -368,3 +376,47 @@ async def test_report_cover_percentage_state(hass):
properties = await reported_properties(hass, "cover.closed")
properties.assert_equal("Alexa.PercentageController", "percentage", 0)
async def test_temperature_sensor_sensor(hass):
"""Test TemperatureSensor reports sensor temperature correctly."""
for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"):
hass.states.async_set(
"sensor.temp_living_room",
bad_value,
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS},
)
properties = await reported_properties(hass, "sensor.temp_living_room")
properties.assert_not_has_property("Alexa.TemperatureSensor", "temperature")
hass.states.async_set(
"sensor.temp_living_room", "34", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}
)
properties = await reported_properties(hass, "sensor.temp_living_room")
properties.assert_equal(
"Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"}
)
async def test_temperature_sensor_climate(hass):
"""Test TemperatureSensor reports climate temperature correctly."""
for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"):
hass.states.async_set(
"climate.downstairs",
climate.HVAC_MODE_HEAT,
{climate.ATTR_CURRENT_TEMPERATURE: bad_value},
)
properties = await reported_properties(hass, "climate.downstairs")
properties.assert_not_has_property("Alexa.TemperatureSensor", "temperature")
hass.states.async_set(
"climate.downstairs",
climate.HVAC_MODE_HEAT,
{climate.ATTR_CURRENT_TEMPERATURE: 34},
)
properties = await reported_properties(hass, "climate.downstairs")
properties.assert_equal(
"Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"}
)

View File

@ -1,6 +1,6 @@
"""Test Alexa config."""
import contextlib
from unittest.mock import patch
from unittest.mock import patch, Mock
from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config
from homeassistant.util.dt import utcnow
@ -43,6 +43,42 @@ async def test_alexa_config_report_state(hass, cloud_prefs):
assert conf.is_reporting_states is False
async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock):
"""Test Alexa config should expose using prefs."""
aioclient_mock.post(
"http://example/alexa_token",
json={
"access_token": "mock-token",
"event_endpoint": "http://example.com/alexa_endpoint",
"expires_in": 30,
},
)
conf = alexa_config.AlexaConfig(
hass,
ALEXA_SCHEMA({}),
cloud_prefs,
Mock(
alexa_access_token_url="http://example/alexa_token",
run_executor=Mock(side_effect=mock_coro),
websession=hass.helpers.aiohttp_client.async_get_clientsession(),
),
)
token = await conf.async_get_access_token()
assert token == "mock-token"
assert len(aioclient_mock.mock_calls) == 1
token = await conf.async_get_access_token()
assert token == "mock-token"
assert len(aioclient_mock.mock_calls) == 1
assert conf._token_valid is not None
conf.async_invalidate_access_token()
assert conf._token_valid is None
token = await conf.async_get_access_token()
assert token == "mock-token"
assert len(aioclient_mock.mock_calls) == 2
@contextlib.contextmanager
def patch_sync_helper():
"""Patch sync helper.

View File

@ -336,6 +336,24 @@ async def test_hassio_update_instance(hass):
assert entry.data[config_flow.CONF_HOST] == "mock-deconz"
async def test_hassio_dont_update_instance(hass):
"""Test we can update an existing config entry."""
entry = MockConfigEntry(
domain=config_flow.DOMAIN,
data={config_flow.CONF_BRIDGEID: "id", config_flow.CONF_HOST: "1.2.3.4"},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
data={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_SERIAL: "id"},
context={"source": "hassio"},
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
async def test_hassio_confirm(hass):
"""Test we can finish a config flow."""
result = await hass.config_entries.flow.async_init(