mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
commit
68d2a1107e
@ -94,6 +94,7 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
self.data_schema = data_schema
|
||||
self.post_write_hook = post_write_hook
|
||||
self.data_validator = data_validator
|
||||
self.mutation_lock = asyncio.Lock()
|
||||
|
||||
def _empty_config(self):
|
||||
"""Empty config if file not found."""
|
||||
@ -114,8 +115,9 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
async def get(self, request, config_key):
|
||||
"""Fetch device specific config."""
|
||||
hass = request.app["hass"]
|
||||
current = await self.read_config(hass)
|
||||
value = self._get_value(hass, current, config_key)
|
||||
async with self.mutation_lock:
|
||||
current = await self.read_config(hass)
|
||||
value = self._get_value(hass, current, config_key)
|
||||
|
||||
if value is None:
|
||||
return self.json_message("Resource not found", 404)
|
||||
@ -148,10 +150,11 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
|
||||
path = hass.config.path(self.path)
|
||||
|
||||
current = await self.read_config(hass)
|
||||
self._write_value(hass, current, config_key, data)
|
||||
async with self.mutation_lock:
|
||||
current = await self.read_config(hass)
|
||||
self._write_value(hass, current, config_key, data)
|
||||
|
||||
await hass.async_add_executor_job(_write, path, current)
|
||||
await hass.async_add_executor_job(_write, path, current)
|
||||
|
||||
if self.post_write_hook is not None:
|
||||
hass.async_create_task(
|
||||
@ -163,15 +166,16 @@ class BaseEditConfigView(HomeAssistantView):
|
||||
async def delete(self, request, config_key):
|
||||
"""Remove an entry."""
|
||||
hass = request.app["hass"]
|
||||
current = await self.read_config(hass)
|
||||
value = self._get_value(hass, current, config_key)
|
||||
path = hass.config.path(self.path)
|
||||
async with self.mutation_lock:
|
||||
current = await self.read_config(hass)
|
||||
value = self._get_value(hass, current, config_key)
|
||||
path = hass.config.path(self.path)
|
||||
|
||||
if value is None:
|
||||
return self.json_message("Resource not found", 404)
|
||||
if value is None:
|
||||
return self.json_message("Resource not found", 404)
|
||||
|
||||
self._delete_value(hass, current, config_key)
|
||||
await hass.async_add_executor_job(_write, path, current)
|
||||
self._delete_value(hass, current, config_key)
|
||||
await hass.async_add_executor_job(_write, path, current)
|
||||
|
||||
if self.post_write_hook is not None:
|
||||
hass.async_create_task(self.post_write_hook(ACTION_DELETE, config_key))
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20200130.2"
|
||||
"home-assistant-frontend==20200130.3"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
@ -133,7 +133,6 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = {
|
||||
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_OPENING): TYPE_SENSOR,
|
||||
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR,
|
||||
(media_player.DOMAIN, media_player.DEVICE_CLASS_TV): TYPE_TV,
|
||||
(media_player.DOMAIN, media_player.DEVICE_CLASS_SPEAKER): TYPE_SPEAKER,
|
||||
(sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE): TYPE_SENSOR,
|
||||
(sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY): TYPE_SENSOR,
|
||||
}
|
||||
@ -146,3 +145,5 @@ STORE_AGENT_USER_IDS = "agent_user_ids"
|
||||
|
||||
SOURCE_CLOUD = "cloud"
|
||||
SOURCE_LOCAL = "local"
|
||||
|
||||
NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK}
|
||||
|
@ -28,6 +28,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
DOMAIN_TO_GOOGLE_TYPES,
|
||||
ERR_FUNCTION_NOT_SUPPORTED,
|
||||
NOT_EXPOSE_LOCAL,
|
||||
SOURCE_LOCAL,
|
||||
STORE_AGENT_USER_IDS,
|
||||
)
|
||||
@ -351,6 +352,18 @@ class GoogleEntity:
|
||||
"""If entity should be exposed."""
|
||||
return self.config.should_expose(self.state)
|
||||
|
||||
@callback
|
||||
def should_expose_local(self) -> bool:
|
||||
"""Return if the entity should be exposed locally."""
|
||||
return (
|
||||
self.should_expose()
|
||||
and get_google_type(
|
||||
self.state.domain, self.state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
)
|
||||
not in NOT_EXPOSE_LOCAL
|
||||
and not self.might_2fa()
|
||||
)
|
||||
|
||||
@callback
|
||||
def is_supported(self) -> bool:
|
||||
"""Return if the entity is supported by Google."""
|
||||
@ -401,7 +414,7 @@ class GoogleEntity:
|
||||
if aliases:
|
||||
device["name"]["nicknames"] = [name] + aliases
|
||||
|
||||
if self.config.is_local_sdk_active:
|
||||
if self.config.is_local_sdk_active and self.should_expose_local():
|
||||
device["otherDeviceIds"] = [{"deviceId": self.entity_id}]
|
||||
device["customData"] = {
|
||||
"webhookId": self.config.local_sdk_webhook_id,
|
||||
|
@ -243,9 +243,7 @@ async def async_devices_reachable(hass, data: RequestData, payload):
|
||||
"devices": [
|
||||
entity.reachable_device_serialize()
|
||||
for entity in async_get_entities(hass, data.config)
|
||||
if entity.entity_id in google_ids
|
||||
and entity.should_expose()
|
||||
and not entity.might_2fa()
|
||||
if entity.entity_id in google_ids and entity.should_expose_local()
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -227,7 +227,11 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if smoke is detected."""
|
||||
return self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF
|
||||
if self._device.smokeDetectorAlarmType:
|
||||
return (
|
||||
self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
|
@ -24,7 +24,10 @@ PRESENCE_SENSOR_TYPES = {
|
||||
}
|
||||
TAG_SENSOR_TYPES = {"Tag Vibration": "vibration", "Tag Open": "opening"}
|
||||
|
||||
SENSOR_TYPES = {"NACamera": WELCOME_SENSOR_TYPES, "NOC": PRESENCE_SENSOR_TYPES}
|
||||
SENSOR_TYPES = {
|
||||
"NACamera": WELCOME_SENSOR_TYPES,
|
||||
"NOC": PRESENCE_SENSOR_TYPES,
|
||||
}
|
||||
|
||||
CONF_HOME = "home"
|
||||
CONF_CAMERAS = "cameras"
|
||||
@ -61,12 +64,28 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
||||
sensor_types.update(SENSOR_TYPES[camera["type"]])
|
||||
|
||||
# Tags are only supported with Netatmo Welcome indoor cameras
|
||||
if camera["type"] == "NACamera" and data.get_modules(camera["id"]):
|
||||
sensor_types.update(TAG_SENSOR_TYPES)
|
||||
modules = data.get_modules(camera["id"])
|
||||
if camera["type"] == "NACamera" and modules:
|
||||
for module in modules:
|
||||
for sensor_type in TAG_SENSOR_TYPES:
|
||||
_LOGGER.debug(
|
||||
"Adding camera tag %s (%s)",
|
||||
module["name"],
|
||||
module["id"],
|
||||
)
|
||||
entities.append(
|
||||
NetatmoBinarySensor(
|
||||
data,
|
||||
camera["id"],
|
||||
home_id,
|
||||
sensor_type,
|
||||
module["id"],
|
||||
)
|
||||
)
|
||||
|
||||
for sensor_name in sensor_types:
|
||||
for sensor_type in sensor_types:
|
||||
entities.append(
|
||||
NetatmoBinarySensor(data, camera["id"], home_id, sensor_name)
|
||||
NetatmoBinarySensor(data, camera["id"], home_id, sensor_type)
|
||||
)
|
||||
except pyatmo.NoDevice:
|
||||
_LOGGER.debug("No camera entities to add")
|
||||
@ -115,6 +134,15 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
"""Return the unique ID for this sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
if self._camera_type == "NACamera":
|
||||
return WELCOME_SENSOR_TYPES.get(self._sensor_type)
|
||||
if self._camera_type == "NOC":
|
||||
return PRESENCE_SENSOR_TYPES.get(self._sensor_type)
|
||||
return TAG_SENSOR_TYPES.get(self._sensor_type)
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info for the sensor."""
|
||||
|
@ -88,7 +88,11 @@ _UNDEF = object()
|
||||
async def async_create_person(hass, name, *, user_id=None, device_trackers=None):
|
||||
"""Create a new person."""
|
||||
await hass.data[DOMAIN][1].async_create_item(
|
||||
{ATTR_NAME: name, ATTR_USER_ID: user_id, "device_trackers": device_trackers}
|
||||
{
|
||||
ATTR_NAME: name,
|
||||
ATTR_USER_ID: user_id,
|
||||
CONF_DEVICE_TRACKERS: device_trackers or [],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -103,14 +107,14 @@ async def async_add_user_device_tracker(
|
||||
if person.get(ATTR_USER_ID) != user_id:
|
||||
continue
|
||||
|
||||
device_trackers = person["device_trackers"]
|
||||
device_trackers = person[CONF_DEVICE_TRACKERS]
|
||||
|
||||
if device_tracker_entity_id in device_trackers:
|
||||
return
|
||||
|
||||
await coll.async_update_item(
|
||||
person[collection.CONF_ID],
|
||||
{"device_trackers": device_trackers + [device_tracker_entity_id]},
|
||||
{CONF_DEVICE_TRACKERS: device_trackers + [device_tracker_entity_id]},
|
||||
)
|
||||
break
|
||||
|
||||
@ -161,6 +165,23 @@ class PersonStorageCollection(collection.StorageCollection):
|
||||
super().__init__(store, logger, id_manager)
|
||||
self.yaml_collection = yaml_collection
|
||||
|
||||
async def _async_load_data(self) -> Optional[dict]:
|
||||
"""Load the data.
|
||||
|
||||
A past bug caused onboarding to create invalid person objects.
|
||||
This patches it up.
|
||||
"""
|
||||
data = await super()._async_load_data()
|
||||
|
||||
if data is None:
|
||||
return data
|
||||
|
||||
for person in data["items"]:
|
||||
if person[CONF_DEVICE_TRACKERS] is None:
|
||||
person[CONF_DEVICE_TRACKERS] = []
|
||||
|
||||
return data
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load the Storage collection."""
|
||||
await super().async_load()
|
||||
@ -179,14 +200,16 @@ class PersonStorageCollection(collection.StorageCollection):
|
||||
return
|
||||
|
||||
for person in list(self.data.values()):
|
||||
if entity_id not in person["device_trackers"]:
|
||||
if entity_id not in person[CONF_DEVICE_TRACKERS]:
|
||||
continue
|
||||
|
||||
await self.async_update_item(
|
||||
person[collection.CONF_ID],
|
||||
{
|
||||
"device_trackers": [
|
||||
devt for devt in person["device_trackers"] if devt != entity_id
|
||||
CONF_DEVICE_TRACKERS: [
|
||||
devt
|
||||
for devt in person[CONF_DEVICE_TRACKERS]
|
||||
if devt != entity_id
|
||||
]
|
||||
},
|
||||
)
|
||||
@ -315,7 +338,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
conf = await entity_component.async_prepare_reload(skip_reset=True)
|
||||
if conf is None:
|
||||
return
|
||||
await yaml_collection.async_load(await filter_yaml_data(hass, conf[DOMAIN]))
|
||||
await yaml_collection.async_load(
|
||||
await filter_yaml_data(hass, conf.get(DOMAIN, []))
|
||||
)
|
||||
|
||||
service.async_register_admin_service(
|
||||
hass, DOMAIN, SERVICE_RELOAD, async_reload_yaml
|
||||
@ -406,7 +431,7 @@ class Person(RestoreEntity):
|
||||
self._unsub_track_device()
|
||||
self._unsub_track_device = None
|
||||
|
||||
trackers = self._config.get(CONF_DEVICE_TRACKERS)
|
||||
trackers = self._config[CONF_DEVICE_TRACKERS]
|
||||
|
||||
if trackers:
|
||||
_LOGGER.debug("Subscribe to device trackers for %s", self.entity_id)
|
||||
@ -426,7 +451,7 @@ class Person(RestoreEntity):
|
||||
def _update_state(self):
|
||||
"""Update the state."""
|
||||
latest_non_gps_home = latest_not_home = latest_gps = latest = None
|
||||
for entity_id in self._config.get(CONF_DEVICE_TRACKERS, []):
|
||||
for entity_id in self._config[CONF_DEVICE_TRACKERS]:
|
||||
state = self.hass.states.get(entity_id)
|
||||
|
||||
if not state or state.state in IGNORE_STATES:
|
||||
|
@ -157,7 +157,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||
@property
|
||||
def media_content_id(self) -> Optional[str]:
|
||||
"""Return the media URL."""
|
||||
return self._currently_playing.get("item", {}).get("name")
|
||||
item = self._currently_playing.get("item") or {}
|
||||
return item.get("name")
|
||||
|
||||
@property
|
||||
def media_content_type(self) -> Optional[str]:
|
||||
@ -203,7 +204,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||
@property
|
||||
def media_title(self) -> Optional[str]:
|
||||
"""Return the media title."""
|
||||
return self._currently_playing.get("item", {}).get("name")
|
||||
item = self._currently_playing.get("item") or {}
|
||||
return item.get("name")
|
||||
|
||||
@property
|
||||
def media_artist(self) -> Optional[str]:
|
||||
@ -224,7 +226,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||
@property
|
||||
def media_track(self) -> Optional[int]:
|
||||
"""Track number of current playing media, music track only."""
|
||||
return self._currently_playing.get("item", {}).get("track_number")
|
||||
item = self._currently_playing.get("item") or {}
|
||||
return item.get("track_number")
|
||||
|
||||
@property
|
||||
def media_playlist(self):
|
||||
|
@ -1,9 +0,0 @@
|
||||
play_playlist:
|
||||
description: Play a Spotify playlist.
|
||||
fields:
|
||||
media_content_id:
|
||||
description: Spotify URI of the playlist.
|
||||
example: 'spotify:playlist:0IpRnqCHSjun48oQRX1Dy7'
|
||||
random_song:
|
||||
description: True to select random song at start, False to start from beginning.
|
||||
example: true
|
@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 105
|
||||
PATCH_VERSION = "3"
|
||||
PATCH_VERSION = "4"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 7, 0)
|
||||
|
@ -158,9 +158,13 @@ class StorageCollection(ObservableCollection):
|
||||
"""Home Assistant object."""
|
||||
return self.store.hass
|
||||
|
||||
async def _async_load_data(self) -> Optional[dict]:
|
||||
"""Load the data."""
|
||||
return cast(Optional[dict], await self.store.async_load())
|
||||
|
||||
async def async_load(self) -> None:
|
||||
"""Load the storage Manager."""
|
||||
raw_storage = cast(Optional[dict], await self.store.async_load())
|
||||
raw_storage = await self._async_load_data()
|
||||
|
||||
if raw_storage is None:
|
||||
raw_storage = {"items": []}
|
||||
|
@ -11,7 +11,7 @@ cryptography==2.8
|
||||
defusedxml==0.6.0
|
||||
distro==1.4.0
|
||||
hass-nabucasa==0.31
|
||||
home-assistant-frontend==20200130.2
|
||||
home-assistant-frontend==20200130.3
|
||||
importlib-metadata==1.4.0
|
||||
jinja2>=2.10.3
|
||||
netdisco==2.6.0
|
||||
|
@ -679,7 +679,7 @@ hole==0.5.0
|
||||
holidays==0.9.12
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20200130.2
|
||||
home-assistant-frontend==20200130.3
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.8
|
||||
|
@ -247,7 +247,7 @@ hole==0.5.0
|
||||
holidays==0.9.12
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20200130.2
|
||||
home-assistant-frontend==20200130.3
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.8
|
||||
|
@ -7,6 +7,7 @@ import pytest
|
||||
from homeassistant.components.google_assistant import helpers
|
||||
from homeassistant.components.google_assistant.const import ( # noqa: F401
|
||||
EVENT_COMMAND_RECEIVED,
|
||||
NOT_EXPOSE_LOCAL,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt
|
||||
@ -46,6 +47,15 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass):
|
||||
"webhookId": "mock-webhook-id",
|
||||
}
|
||||
|
||||
for device_type in NOT_EXPOSE_LOCAL:
|
||||
with patch(
|
||||
"homeassistant.components.google_assistant.helpers.get_google_type",
|
||||
return_value=device_type,
|
||||
):
|
||||
serialized = await entity.sync_serialize(None)
|
||||
assert "otherDeviceIds" not in serialized
|
||||
assert "customData" not in serialized
|
||||
|
||||
|
||||
async def test_config_local_sdk(hass, hass_client):
|
||||
"""Test the local SDK."""
|
||||
|
@ -682,7 +682,6 @@ async def test_device_class_cover(hass, device_class, google_type):
|
||||
"device_class,google_type",
|
||||
[
|
||||
("non_existing_class", "action.devices.types.SWITCH"),
|
||||
("speaker", "action.devices.types.SPEAKER"),
|
||||
("tv", "action.devices.types.TV"),
|
||||
],
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""The tests for the person component."""
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
|
||||
from asynctest import patch
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import person
|
||||
@ -773,3 +773,15 @@ async def test_reload(hass, hass_admin_user):
|
||||
assert state_2 is None
|
||||
assert state_3 is not None
|
||||
assert state_3.name == "Person 3"
|
||||
|
||||
|
||||
async def test_person_storage_fixing_device_trackers(storage_collection):
|
||||
"""Test None device trackers become lists."""
|
||||
with patch.object(
|
||||
storage_collection.store,
|
||||
"async_load",
|
||||
return_value={"items": [{"id": "bla", "name": "bla", "device_trackers": None}]},
|
||||
):
|
||||
await storage_collection.async_load()
|
||||
|
||||
assert storage_collection.data["bla"]["device_trackers"] == []
|
||||
|
Loading…
x
Reference in New Issue
Block a user