Merge pull request #31841 from home-assistant/rc

0.105.4
This commit is contained in:
Paulus Schoutsen 2020-02-14 16:29:09 -08:00 committed by GitHub
commit 68d2a1107e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 144 additions and 52 deletions

View File

@ -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))

View File

@ -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",

View File

@ -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}

View File

@ -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,

View File

@ -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()
]
}

View File

@ -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):

View File

@ -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."""

View File

@ -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:

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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": []}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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."""

View File

@ -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"),
],
)

View File

@ -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"] == []