Merge pull request #12334 from home-assistant/release-0-63-1

0.63.1
This commit is contained in:
Paulus Schoutsen 2018-02-12 08:56:04 -08:00 committed by GitHub
commit 073126755c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 210 additions and 55 deletions

View File

@ -328,8 +328,9 @@ class _AlexaBrightnessController(_AlexaInterface):
def get_property(self, name):
if name != 'brightness':
raise _UnsupportedProperty(name)
return round(self.entity.attributes['brightness'] / 255.0 * 100)
if 'brightness' in self.entity.attributes:
return round(self.entity.attributes['brightness'] / 255.0 * 100)
return 0
class _AlexaColorController(_AlexaInterface):
@ -1064,7 +1065,16 @@ def async_api_lock(hass, config, request, entity):
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
return api_message(request)
# Alexa expects a lockState in the response, we don't know the actual
# lockState at this point but assume it is locked. It is reported
# correctly later when ReportState is called. The alt. to this approach
# is to implement DeferredResponse
properties = [{
'name': 'lockState',
'namespace': 'Alexa.LockController',
'value': 'LOCKED'
}]
return api_message(request, context={'properties': properties})
# Not supported by Alexa yet
@ -1168,7 +1178,7 @@ def async_api_adjust_volume(hass, config, request, entity):
@asyncio.coroutine
def async_api_adjust_volume_step(hass, config, request, entity):
"""Process an adjust volume step request."""
volume_step = round(float(request[API_PAYLOAD]['volume'] / 100), 2)
volume_step = round(float(request[API_PAYLOAD]['volumeSteps'] / 100), 2)
current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)

View File

@ -131,8 +131,6 @@ class NetatmoBinarySensor(BinarySensorDevice):
self._name += ' / ' + module_name
self._sensor_name = sensor
self._name += ' ' + sensor
self._unique_id = data.camera_data.cameraByName(
camera=camera_name, home=home)['id']
self._cameratype = camera_type
self._state = None
@ -141,11 +139,6 @@ class NetatmoBinarySensor(BinarySensorDevice):
"""Return the name of the Netatmo device and this sensor."""
return self._name
@property
def unique_id(self):
"""Return the unique ID for this sensor."""
return self._unique_id
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""

View File

@ -67,8 +67,6 @@ class NetatmoCamera(Camera):
self._vpnurl, self._localurl = self._data.camera_data.cameraUrls(
camera=camera_name
)
self._unique_id = data.camera_data.cameraByName(
camera=camera_name, home=home)['id']
self._cameratype = camera_type
def camera_image(self):
@ -112,8 +110,3 @@ class NetatmoCamera(Camera):
elif self._cameratype == "NACamera":
return "Welcome"
return None
@property
def unique_id(self):
"""Return the unique ID for this camera."""
return self._unique_id

View File

@ -226,7 +226,6 @@ class XiaomiMiioRemote(RemoteDevice):
_LOGGER.error("Device does not support turn_off, " +
"please use 'remote.send_command' to send commands.")
# pylint: enable=R0201
def _send_command(self, payload):
"""Send a command."""
from miio import DeviceException

View File

@ -113,18 +113,12 @@ class NetAtmoSensor(Entity):
module_id = self.netatmo_data.\
station_data.moduleByName(module=module_name)['_id']
self.module_id = module_id[1]
self._unique_id = '{}-{}'.format(self.module_id, self.type)
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def unique_id(self):
"""Return the unique ID for this sensor."""
return self._unique_id
@property
def icon(self):
"""Icon to use in the frontend, if any."""

View File

@ -18,7 +18,7 @@ from homeassistant.util import slugify
REQUIREMENTS = [
'bellows==0.5.0',
'zigpy==0.0.1',
'zigpy-xbee==0.0.1',
'zigpy-xbee==0.0.2',
]
DOMAIN = 'zha'

View File

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

View File

@ -80,6 +80,9 @@ class Entity(object):
# Process updates in parallel
parallel_updates = None
# Name in the entity registry
registry_name = None
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state.
@ -225,7 +228,7 @@ class Entity(object):
if unit_of_measurement is not None:
attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement
name = self.name
name = self.registry_name or self.name
if name is not None:
attr[ATTR_FRIENDLY_NAME] = name

View File

@ -40,19 +40,19 @@ class EntityComponent(object):
self.config = None
self._platforms = {
'core': EntityPlatform(
domain: EntityPlatform(
hass=hass,
logger=logger,
domain=domain,
platform_name='core',
platform_name=domain,
scan_interval=self.scan_interval,
parallel_updates=0,
entity_namespace=None,
async_entities_added_callback=self._async_update_group,
)
}
self.async_add_entities = self._platforms['core'].async_add_entities
self.add_entities = self._platforms['core'].add_entities
self.async_add_entities = self._platforms[domain].async_add_entities
self.add_entities = self._platforms[domain].add_entities
@property
def entities(self):
@ -190,7 +190,7 @@ class EntityComponent(object):
yield from asyncio.wait(tasks, loop=self.hass.loop)
self._platforms = {
'core': self._platforms['core']
self.domain: self._platforms[self.domain]
}
self.config = None

View File

@ -209,10 +209,15 @@ class EntityPlatform(object):
else:
suggested_object_id = entity.name
if self.entity_namespace is not None:
suggested_object_id = '{} {}'.format(
self.entity_namespace, suggested_object_id)
entry = registry.async_get_or_create(
self.domain, self.platform_name, entity.unique_id,
suggested_object_id=suggested_object_id)
entity.entity_id = entry.entity_id
entity.registry_name = entry.name
# We won't generate an entity ID if the platform has already set one
# We will however make sure that platform cannot pick a registered ID
@ -239,8 +244,12 @@ class EntityPlatform(object):
raise HomeAssistantError(
'Invalid entity id: {}'.format(entity.entity_id))
elif entity.entity_id in component_entities:
msg = 'Entity id already exists: {}'.format(entity.entity_id)
if entity.unique_id is not None:
msg += '. Platform {} does not generate unique IDs'.format(
self.platform_name)
raise HomeAssistantError(
'Entity id already exists: {}'.format(entity.entity_id))
msg)
self.entities[entity.entity_id] = entity
component_entities.add(entity.entity_id)

View File

@ -11,22 +11,37 @@ After initializing, call EntityRegistry.async_ensure_loaded to load the data
from disk.
"""
import asyncio
from collections import namedtuple, OrderedDict
from collections import OrderedDict
from itertools import chain
import logging
import os
import attr
from ..core import callback, split_entity_id
from ..util import ensure_unique_string, slugify
from ..util.yaml import load_yaml, save_yaml
PATH_REGISTRY = 'entity_registry.yaml'
SAVE_DELAY = 10
Entry = namedtuple('EntityRegistryEntry',
'entity_id,unique_id,platform,domain')
_LOGGER = logging.getLogger(__name__)
@attr.s(slots=True, frozen=True)
class RegistryEntry:
"""Entity Registry Entry."""
entity_id = attr.ib(type=str)
unique_id = attr.ib(type=str)
platform = attr.ib(type=str)
name = attr.ib(type=str, default=None)
domain = attr.ib(type=str, default=None, init=False, repr=False)
def __attrs_post_init__(self):
"""Computed properties."""
object.__setattr__(self, "domain", split_entity_id(self.entity_id)[0])
class EntityRegistry:
"""Class to hold a registry of entities."""
@ -65,11 +80,10 @@ class EntityRegistry:
entity_id = self.async_generate_entity_id(
domain, suggested_object_id or '{}_{}'.format(platform, unique_id))
entity = Entry(
entity = RegistryEntry(
entity_id=entity_id,
unique_id=unique_id,
platform=platform,
domain=domain,
)
self.entities[entity_id] = entity
_LOGGER.info('Registered new %s.%s entity: %s',
@ -98,11 +112,11 @@ class EntityRegistry:
data = yield from self.hass.async_add_job(load_yaml, path)
for entity_id, info in data.items():
entities[entity_id] = Entry(
domain=split_entity_id(entity_id)[0],
entities[entity_id] = RegistryEntry(
entity_id=entity_id,
unique_id=info['unique_id'],
platform=info['platform']
platform=info['platform'],
name=info.get('name')
)
self.entities = entities

View File

@ -11,6 +11,7 @@ async_timeout==2.0.0
chardet==3.0.4
astral==1.5
certifi>=2017.4.17
attrs==17.4.0
# Breaks Python 3.6 and is not needed for our supported Pythons
enum34==1000000000.0.0

View File

@ -206,7 +206,7 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
return platform
try:
yield from _process_deps_reqs(hass, config, platform_name, platform)
yield from _process_deps_reqs(hass, config, platform_path, platform)
except HomeAssistantError as err:
log_error(str(err))
return None

View File

@ -12,6 +12,7 @@ async_timeout==2.0.0
chardet==3.0.4
astral==1.5
certifi>=2017.4.17
attrs==17.4.0
# homeassistant.components.nuimo_controller
--only-binary=all https://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0
@ -1275,7 +1276,7 @@ zeroconf==0.19.1
ziggo-mediabox-xl==1.0.0
# homeassistant.components.zha
zigpy-xbee==0.0.1
zigpy-xbee==0.0.2
# homeassistant.components.zha
zigpy==0.0.1

View File

@ -61,6 +61,7 @@ REQUIRES = [
'chardet==3.0.4',
'astral==1.5',
'certifi>=2017.4.17',
'attrs==17.4.0',
]
MIN_PY_VERSION = '.'.join(map(

View File

@ -317,10 +317,10 @@ def mock_component(hass, component):
hass.config.components.add(component)
def mock_registry(hass):
def mock_registry(hass, mock_entries=None):
"""Mock the Entity Registry."""
registry = entity_registry.EntityRegistry(hass)
registry.entities = {}
registry.entities = mock_entries or {}
hass.data[entity_platform.DATA_REGISTRY] = registry
return registry

View File

@ -401,11 +401,17 @@ def test_lock(hass):
assert appliance['friendlyName'] == "Test lock"
assert_endpoint_capabilities(appliance, 'Alexa.LockController')
yield from assert_request_calls_service(
_, msg = yield from assert_request_calls_service(
'Alexa.LockController', 'Lock', 'lock#test',
'lock.lock',
hass)
# always return LOCKED for now
properties = msg['context']['properties'][0]
assert properties['name'] == 'lockState'
assert properties['namespace'] == 'Alexa.LockController'
assert properties['value'] == 'LOCKED'
@asyncio.coroutine
def test_media_player(hass):
@ -511,14 +517,14 @@ def test_media_player(hass):
'Alexa.StepSpeaker', 'AdjustVolume', 'media_player#test',
'media_player.volume_set',
hass,
payload={'volume': 20})
payload={'volumeSteps': 20})
assert call.data['volume_level'] == 0.95
call, _ = yield from assert_request_calls_service(
'Alexa.StepSpeaker', 'AdjustVolume', 'media_player#test',
'media_player.volume_set',
hass,
payload={'volume': -20})
payload={'volumeSteps': -20})
assert call.data['volume_level'] == 0.55
@ -1095,6 +1101,23 @@ def test_report_lock_state(hass):
properties.assert_equal('Alexa.LockController', 'lockState', 'JAMMED')
@asyncio.coroutine
def test_report_dimmable_light_state(hass):
"""Test BrightnessController reports brightness correctly."""
hass.states.async_set(
'light.test_on', 'on', {'friendly_name': "Test light On",
'brightness': 128, 'supported_features': 1})
hass.states.async_set(
'light.test_off', 'off', {'friendly_name': "Test light Off",
'supported_features': 1})
properties = yield from reported_properties(hass, 'light.test_on')
properties.assert_equal('Alexa.BrightnessController', 'brightness', 50)
properties = yield from reported_properties(hass, 'light.test_off')
properties.assert_equal('Alexa.BrightnessController', 'brightness', 0)
@asyncio.coroutine
def reported_properties(hass, endpoint):
"""Use ReportState to get properties and return them.
@ -1118,7 +1141,7 @@ class _ReportedProperties(object):
for prop in self.properties:
if prop['namespace'] == namespace and prop['name'] == name:
assert prop['value'] == value
return prop
return prop
assert False, 'property %s:%s not in %r' % (
namespace,

View File

@ -12,7 +12,7 @@ import homeassistant.loader as loader
from homeassistant.exceptions import PlatformNotReady
from homeassistant.components import group
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.setup import setup_component
from homeassistant.setup import setup_component, async_setup_component
from homeassistant.helpers import discovery
import homeassistant.util.dt as dt_util
@ -305,3 +305,31 @@ def test_extract_from_service_no_group_expand(hass):
extracted = component.async_extract_from_service(call, expand_group=False)
assert extracted == [test_group]
@asyncio.coroutine
def test_setup_dependencies_platform(hass):
"""Test we setup the dependencies of a platform.
We're explictely testing that we process dependencies even if a component
with the same name has already been loaded.
"""
loader.set_component('test_component', MockModule('test_component'))
loader.set_component('test_component2', MockModule('test_component2'))
loader.set_component(
'test_domain.test_component',
MockPlatform(dependencies=['test_component', 'test_component2']))
component = EntityComponent(_LOGGER, DOMAIN, hass)
yield from async_setup_component(hass, 'test_component', {})
yield from component.async_setup({
DOMAIN: {
'platform': 'test_component',
}
})
assert 'test_component' in hass.config.components
assert 'test_component2' in hass.config.components
assert 'test_domain.test_component' in hass.config.components

View File

@ -9,7 +9,7 @@ import homeassistant.loader as loader
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.entity_component import (
EntityComponent, DEFAULT_SCAN_INTERVAL)
from homeassistant.helpers import entity_platform
from homeassistant.helpers import entity_platform, entity_registry
import homeassistant.util.dt as dt_util
@ -21,6 +21,32 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "test_domain"
class MockEntityPlatform(entity_platform.EntityPlatform):
"""Mock class with some mock defaults."""
def __init__(
self, *, hass,
logger=None,
domain='test',
platform_name='test_platform',
scan_interval=timedelta(seconds=15),
parallel_updates=0,
entity_namespace=None,
async_entities_added_callback=lambda: None
):
"""Initialize a mock entity platform."""
super().__init__(
hass=hass,
logger=logger,
domain=domain,
platform_name=platform_name,
scan_interval=scan_interval,
parallel_updates=parallel_updates,
entity_namespace=entity_namespace,
async_entities_added_callback=async_entities_added_callback,
)
class TestHelpersEntityPlatform(unittest.TestCase):
"""Test homeassistant.helpers.entity_component module."""
@ -433,3 +459,34 @@ def test_entity_with_name_and_entity_id_getting_registered(hass):
MockEntity(unique_id='1234', name='bla',
entity_id='test_domain.world')])
assert 'test_domain.world' in hass.states.async_entity_ids()
@asyncio.coroutine
def test_overriding_name_from_registry(hass):
"""Test that we can override a name via the Entity Registry."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
mock_registry(hass, {
'test_domain.world': entity_registry.RegistryEntry(
entity_id='test_domain.world',
unique_id='1234',
# Using component.async_add_entities is equal to platform "domain"
platform='test_domain',
name='Overridden'
)
})
yield from component.async_add_entities([
MockEntity(unique_id='1234', name='Device Name')])
state = hass.states.get('test_domain.world')
assert state is not None
assert state.name == 'Overridden'
@asyncio.coroutine
def test_registry_respect_entity_namespace(hass):
"""Test that the registry respects entity namespace."""
mock_registry(hass)
platform = MockEntityPlatform(hass=hass, entity_namespace='ns')
entity = MockEntity(unique_id='1234', name='Device Name')
yield from platform.async_add_entities([entity])
assert entity.entity_id == 'test.ns_device_name'

View File

@ -9,6 +9,9 @@ from homeassistant.helpers import entity_registry
from tests.common import mock_registry
YAML__OPEN_PATH = 'homeassistant.util.yaml.open'
@pytest.fixture
def registry(hass):
"""Return an empty, loaded, registry."""
@ -82,13 +85,12 @@ def test_save_timer_reset_on_subsequent_save(hass, registry):
@asyncio.coroutine
def test_loading_saving_data(hass, registry):
"""Test that we load/save data correctly."""
yaml_path = 'homeassistant.util.yaml.open'
orig_entry1 = registry.async_get_or_create('light', 'hue', '1234')
orig_entry2 = registry.async_get_or_create('light', 'hue', '5678')
assert len(registry.entities) == 2
with patch(yaml_path, mock_open(), create=True) as mock_write:
with patch(YAML__OPEN_PATH, mock_open(), create=True) as mock_write:
yield from registry._async_save()
# Mock open calls are: open file, context enter, write, context leave
@ -98,7 +100,7 @@ def test_loading_saving_data(hass, registry):
registry2 = entity_registry.EntityRegistry(hass)
with patch('os.path.isfile', return_value=True), \
patch(yaml_path, mock_open(read_data=written), create=True):
patch(YAML__OPEN_PATH, mock_open(read_data=written), create=True):
yield from registry2._async_load()
# Ensure same order
@ -133,3 +135,30 @@ def test_is_registered(registry):
entry = registry.async_get_or_create('light', 'hue', '1234')
assert registry.async_is_registered(entry.entity_id)
assert not registry.async_is_registered('light.non_existing')
@asyncio.coroutine
def test_loading_extra_values(hass):
"""Test we load extra data from the registry."""
written = """
test.named:
platform: super_platform
unique_id: with-name
name: registry override
test.no_name:
platform: super_platform
unique_id: without-name
"""
registry = entity_registry.EntityRegistry(hass)
with patch('os.path.isfile', return_value=True), \
patch(YAML__OPEN_PATH, mock_open(read_data=written), create=True):
yield from registry._async_load()
entry_with_name = registry.async_get_or_create(
'test', 'super_platform', 'with-name')
entry_without_name = registry.async_get_or_create(
'test', 'super_platform', 'without-name')
assert entry_with_name.name == 'registry override'
assert entry_without_name.name is None