mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
commit
8ae5ece6bb
@ -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:
|
||||
|
@ -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]}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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": []
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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"}
|
||||
)
|
||||
|
@ -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.
|
||||
|
@ -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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user