mirror of
https://github.com/home-assistant/core.git
synced 2025-09-21 02:49:32 +00:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a69938afa2 | ||
![]() |
94c3d9bac0 | ||
![]() |
fb7af0384f | ||
![]() |
5a9a95abe4 | ||
![]() |
e1ad108b6d | ||
![]() |
a76620b76f | ||
![]() |
13fd80affa | ||
![]() |
d576749530 | ||
![]() |
111a00aeeb | ||
![]() |
b3d5717df8 | ||
![]() |
a84378bb79 | ||
![]() |
53ba45cc8f | ||
![]() |
5d77eb1839 | ||
![]() |
517159dc4e | ||
![]() |
a9287b7117 | ||
![]() |
0b7bcc87df | ||
![]() |
482661f82c |
@@ -40,6 +40,16 @@ def async_setup(hass):
|
||||
hass.http.register_view(AlexaIntentsView)
|
||||
|
||||
|
||||
async def async_setup_intents(hass):
|
||||
"""
|
||||
Do intents setup.
|
||||
|
||||
Right now this module does not expose any, but the intent component breaks
|
||||
without it.
|
||||
"""
|
||||
pass # pylint: disable=unnecessary-pass
|
||||
|
||||
|
||||
class UnknownRequest(HomeAssistantError):
|
||||
"""When an unknown Alexa request is passed in."""
|
||||
|
||||
|
@@ -232,7 +232,7 @@ NODE_FILTERS = {
|
||||
"RemoteLinc2_ADV",
|
||||
],
|
||||
FILTER_INSTEON_TYPE: ["0.16.", "0.17.", "0.18.", "9.0.", "9.7."],
|
||||
FILTER_ZWAVE_CAT: (["118", "143"] + list(map(str, range(180, 185)))),
|
||||
FILTER_ZWAVE_CAT: (["118", "143"] + list(map(str, range(180, 186)))),
|
||||
},
|
||||
LOCK: {
|
||||
FILTER_UOM: ["11"],
|
||||
|
@@ -3,5 +3,5 @@
|
||||
"name": "KEF",
|
||||
"documentation": "https://www.home-assistant.io/integrations/kef",
|
||||
"codeowners": ["@basnijholt"],
|
||||
"requirements": ["aiokef==0.2.9", "getmac==0.8.2"]
|
||||
"requirements": ["aiokef==0.2.10", "getmac==0.8.2"]
|
||||
}
|
||||
|
@@ -56,7 +56,6 @@ ERR_ENCRYPTION_ALREADY_ENABLED = "encryption_already_enabled"
|
||||
ERR_ENCRYPTION_NOT_AVAILABLE = "encryption_not_available"
|
||||
ERR_ENCRYPTION_REQUIRED = "encryption_required"
|
||||
ERR_SENSOR_NOT_REGISTERED = "not_registered"
|
||||
ERR_SENSOR_DUPLICATE_UNIQUE_ID = "duplicate_unique_id"
|
||||
ERR_INVALID_FORMAT = "invalid_format"
|
||||
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Webhook handlers for mobile_app."""
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
import logging
|
||||
import secrets
|
||||
@@ -28,7 +29,7 @@ from homeassistant.const import (
|
||||
HTTP_CREATED,
|
||||
)
|
||||
from homeassistant.core import EventOrigin
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError
|
||||
from homeassistant.exceptions import ServiceNotFound, TemplateError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.template import attach
|
||||
@@ -77,7 +78,6 @@ from .const import (
|
||||
ERR_ENCRYPTION_NOT_AVAILABLE,
|
||||
ERR_ENCRYPTION_REQUIRED,
|
||||
ERR_INVALID_FORMAT,
|
||||
ERR_SENSOR_DUPLICATE_UNIQUE_ID,
|
||||
ERR_SENSOR_NOT_REGISTERED,
|
||||
SIGNAL_LOCATION_UPDATE,
|
||||
SIGNAL_SENSOR_UPDATE,
|
||||
@@ -95,6 +95,7 @@ from .helpers import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DELAY_SAVE = 10
|
||||
|
||||
WEBHOOK_COMMANDS = Registry()
|
||||
|
||||
@@ -184,7 +185,10 @@ async def handle_webhook(
|
||||
"Received webhook payload for type %s: %s", webhook_type, webhook_payload
|
||||
)
|
||||
|
||||
return await WEBHOOK_COMMANDS[webhook_type](hass, config_entry, webhook_payload)
|
||||
# Shield so we make sure we finish the webhook, even if sender hangs up.
|
||||
return await asyncio.shield(
|
||||
WEBHOOK_COMMANDS[webhook_type](hass, config_entry, webhook_payload)
|
||||
)
|
||||
|
||||
|
||||
@WEBHOOK_COMMANDS.register("call_service")
|
||||
@@ -352,38 +356,39 @@ async def webhook_enable_encryption(hass, config_entry, data):
|
||||
vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES),
|
||||
vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string,
|
||||
vol.Optional(ATTR_SENSOR_UOM): cv.string,
|
||||
vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float),
|
||||
vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any(
|
||||
None, bool, str, int, float
|
||||
),
|
||||
vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon,
|
||||
}
|
||||
)
|
||||
async def webhook_register_sensor(hass, config_entry, data):
|
||||
"""Handle a register sensor webhook."""
|
||||
entity_type = data[ATTR_SENSOR_TYPE]
|
||||
|
||||
unique_id = data[ATTR_SENSOR_UNIQUE_ID]
|
||||
|
||||
unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}"
|
||||
|
||||
if unique_store_key in hass.data[DOMAIN][entity_type]:
|
||||
_LOGGER.error("Refusing to re-register existing sensor %s!", unique_id)
|
||||
return error_response(
|
||||
ERR_SENSOR_DUPLICATE_UNIQUE_ID,
|
||||
f"{entity_type} {unique_id} already exists!",
|
||||
status=409,
|
||||
)
|
||||
existing_sensor = unique_store_key in hass.data[DOMAIN][entity_type]
|
||||
|
||||
data[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID]
|
||||
|
||||
# If sensor already is registered, update current state instead
|
||||
if existing_sensor:
|
||||
_LOGGER.debug("Re-register existing sensor %s", unique_id)
|
||||
entry = hass.data[DOMAIN][entity_type][unique_store_key]
|
||||
data = {**entry, **data}
|
||||
|
||||
hass.data[DOMAIN][entity_type][unique_store_key] = data
|
||||
|
||||
try:
|
||||
await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass))
|
||||
except HomeAssistantError as ex:
|
||||
_LOGGER.error("Error registering sensor: %s", ex)
|
||||
return empty_okay_response()
|
||||
hass.data[DOMAIN][DATA_STORE].async_delay_save(
|
||||
lambda: savable_state(hass), DELAY_SAVE
|
||||
)
|
||||
|
||||
register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register"
|
||||
async_dispatcher_send(hass, register_signal, data)
|
||||
if existing_sensor:
|
||||
async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, data)
|
||||
else:
|
||||
register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register"
|
||||
async_dispatcher_send(hass, register_signal, data)
|
||||
|
||||
return webhook_response(
|
||||
{"success": True}, registration=config_entry.data, status=HTTP_CREATED,
|
||||
@@ -414,7 +419,7 @@ async def webhook_update_sensor_states(hass, config_entry, data):
|
||||
{
|
||||
vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict,
|
||||
vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon,
|
||||
vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float),
|
||||
vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, str, int, float),
|
||||
vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES),
|
||||
vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string,
|
||||
}
|
||||
@@ -458,18 +463,14 @@ async def webhook_update_sensor_states(hass, config_entry, data):
|
||||
|
||||
hass.data[DOMAIN][entity_type][unique_store_key] = new_state
|
||||
|
||||
safe = savable_state(hass)
|
||||
|
||||
try:
|
||||
await hass.data[DOMAIN][DATA_STORE].async_save(safe)
|
||||
except HomeAssistantError as ex:
|
||||
_LOGGER.error("Error updating mobile_app registration: %s", ex)
|
||||
return empty_okay_response()
|
||||
|
||||
async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state)
|
||||
|
||||
resp[unique_id] = {"success": True}
|
||||
|
||||
hass.data[DOMAIN][DATA_STORE].async_delay_save(
|
||||
lambda: savable_state(hass), DELAY_SAVE
|
||||
)
|
||||
|
||||
return webhook_response(resp, registration=config_entry.data)
|
||||
|
||||
|
||||
|
@@ -210,6 +210,10 @@ class NanoleafLight(LightEntity):
|
||||
self._light.brightness = int(brightness / 2.55)
|
||||
|
||||
if effect:
|
||||
if effect not in self._effects_list:
|
||||
raise ValueError(
|
||||
f"Attempting to apply effect not in the effect list: '{effect}'"
|
||||
)
|
||||
self._light.effect = effect
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
@@ -227,8 +231,13 @@ class NanoleafLight(LightEntity):
|
||||
self._available = self._light.available
|
||||
self._brightness = self._light.brightness
|
||||
self._color_temp = self._light.color_temperature
|
||||
self._effect = self._light.effect
|
||||
self._effects_list = self._light.effects
|
||||
# Nanoleaf api returns non-existent effect named "*Solid*" when light set to solid color.
|
||||
# This causes various issues with scening (see https://github.com/home-assistant/core/issues/36359).
|
||||
# Until fixed at the library level, we should ensure the effect exists before saving to light properties
|
||||
self._effect = (
|
||||
self._light.effect if self._light.effect in self._effects_list else None
|
||||
)
|
||||
self._hs_color = self._light.hue, self._light.saturation
|
||||
self._state = self._light.on
|
||||
except Unavailable as err:
|
||||
|
@@ -161,7 +161,7 @@ def load_games(hass: HomeAssistantType, unique_id: str) -> dict:
|
||||
"""Load games for sources."""
|
||||
g_file = hass.config.path(GAMES_FILE.format(unique_id))
|
||||
try:
|
||||
games = load_json(g_file, dict)
|
||||
games = load_json(g_file)
|
||||
except HomeAssistantError as error:
|
||||
games = {}
|
||||
_LOGGER.error("Failed to load games file: %s", error)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 111
|
||||
PATCH_VERSION = "0b3"
|
||||
PATCH_VERSION = "0b5"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 7, 0)
|
||||
|
@@ -197,7 +197,7 @@ aioimaplib==0.7.15
|
||||
aiokafka==0.5.1
|
||||
|
||||
# homeassistant.components.kef
|
||||
aiokef==0.2.9
|
||||
aiokef==0.2.10
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==0.6.7
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import UNIT_PERCENTAGE
|
||||
from homeassistant.const import STATE_UNKNOWN, UNIT_PERCENTAGE
|
||||
from homeassistant.helpers import device_registry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -95,8 +95,8 @@ async def test_sensor_must_register(hass, create_registrations, webhook_client):
|
||||
assert json["battery_state"]["error"]["code"] == "not_registered"
|
||||
|
||||
|
||||
async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client):
|
||||
"""Test that sensors must have a unique ID."""
|
||||
async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, caplog):
|
||||
"""Test that a duplicate unique ID in registration updates the sensor."""
|
||||
webhook_id = create_registrations[1]["webhook_id"]
|
||||
webhook_url = f"/api/webhook/{webhook_id}"
|
||||
|
||||
@@ -120,11 +120,140 @@ async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client):
|
||||
|
||||
reg_json = await reg_resp.json()
|
||||
assert reg_json == {"success": True}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "Re-register existing sensor" not in caplog.text
|
||||
|
||||
entity = hass.states.get("sensor.test_1_battery_state")
|
||||
assert entity is not None
|
||||
|
||||
assert entity.attributes["device_class"] == "battery"
|
||||
assert entity.attributes["icon"] == "mdi:battery"
|
||||
assert entity.attributes["unit_of_measurement"] == UNIT_PERCENTAGE
|
||||
assert entity.attributes["foo"] == "bar"
|
||||
assert entity.domain == "sensor"
|
||||
assert entity.name == "Test 1 Battery State"
|
||||
assert entity.state == "100"
|
||||
|
||||
payload["data"]["state"] = 99
|
||||
dupe_resp = await webhook_client.post(webhook_url, json=payload)
|
||||
|
||||
assert dupe_resp.status == 409
|
||||
assert dupe_resp.status == 201
|
||||
dupe_reg_json = await dupe_resp.json()
|
||||
assert dupe_reg_json == {"success": True}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
dupe_json = await dupe_resp.json()
|
||||
assert dupe_json["success"] is False
|
||||
assert dupe_json["error"]["code"] == "duplicate_unique_id"
|
||||
assert "Re-register existing sensor" in caplog.text
|
||||
|
||||
entity = hass.states.get("sensor.test_1_battery_state")
|
||||
assert entity is not None
|
||||
|
||||
assert entity.attributes["device_class"] == "battery"
|
||||
assert entity.attributes["icon"] == "mdi:battery"
|
||||
assert entity.attributes["unit_of_measurement"] == UNIT_PERCENTAGE
|
||||
assert entity.attributes["foo"] == "bar"
|
||||
assert entity.domain == "sensor"
|
||||
assert entity.name == "Test 1 Battery State"
|
||||
assert entity.state == "99"
|
||||
|
||||
|
||||
async def test_register_sensor_no_state(hass, create_registrations, webhook_client):
|
||||
"""Test that sensors can be registered, when there is no (unknown) state."""
|
||||
webhook_id = create_registrations[1]["webhook_id"]
|
||||
webhook_url = f"/api/webhook/{webhook_id}"
|
||||
|
||||
reg_resp = await webhook_client.post(
|
||||
webhook_url,
|
||||
json={
|
||||
"type": "register_sensor",
|
||||
"data": {
|
||||
"name": "Battery State",
|
||||
"state": None,
|
||||
"type": "sensor",
|
||||
"unique_id": "battery_state",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert reg_resp.status == 201
|
||||
|
||||
json = await reg_resp.json()
|
||||
assert json == {"success": True}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity = hass.states.get("sensor.test_1_battery_state")
|
||||
assert entity is not None
|
||||
|
||||
assert entity.domain == "sensor"
|
||||
assert entity.name == "Test 1 Battery State"
|
||||
assert entity.state == STATE_UNKNOWN
|
||||
|
||||
reg_resp = await webhook_client.post(
|
||||
webhook_url,
|
||||
json={
|
||||
"type": "register_sensor",
|
||||
"data": {
|
||||
"name": "Backup Battery State",
|
||||
"type": "sensor",
|
||||
"unique_id": "backup_battery_state",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert reg_resp.status == 201
|
||||
|
||||
json = await reg_resp.json()
|
||||
assert json == {"success": True}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity = hass.states.get("sensor.test_1_backup_battery_state")
|
||||
assert entity
|
||||
|
||||
assert entity.domain == "sensor"
|
||||
assert entity.name == "Test 1 Backup Battery State"
|
||||
assert entity.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
async def test_update_sensor_no_state(hass, create_registrations, webhook_client):
|
||||
"""Test that sensors can be updated, when there is no (unknown) state."""
|
||||
webhook_id = create_registrations[1]["webhook_id"]
|
||||
webhook_url = f"/api/webhook/{webhook_id}"
|
||||
|
||||
reg_resp = await webhook_client.post(
|
||||
webhook_url,
|
||||
json={
|
||||
"type": "register_sensor",
|
||||
"data": {
|
||||
"name": "Battery State",
|
||||
"state": 100,
|
||||
"type": "sensor",
|
||||
"unique_id": "battery_state",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert reg_resp.status == 201
|
||||
|
||||
json = await reg_resp.json()
|
||||
assert json == {"success": True}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity = hass.states.get("sensor.test_1_battery_state")
|
||||
assert entity is not None
|
||||
assert entity.state == "100"
|
||||
|
||||
update_resp = await webhook_client.post(
|
||||
webhook_url,
|
||||
json={
|
||||
"type": "update_sensor_states",
|
||||
"data": [{"state": None, "type": "sensor", "unique_id": "battery_state"}],
|
||||
},
|
||||
)
|
||||
|
||||
assert update_resp.status == 200
|
||||
|
||||
json = await update_resp.json()
|
||||
assert json == {"battery_state": {"success": True}}
|
||||
|
||||
updated_entity = hass.states.get("sensor.test_1_battery_state")
|
||||
assert updated_entity.state == STATE_UNKNOWN
|
||||
|
Reference in New Issue
Block a user