Compare commits

..

17 Commits

Author SHA1 Message Date
Paulus Schoutsen d85ed8d0fe Merge pull request #12402 from home-assistant/release-0-63-2
0.63.2
2018-02-13 23:00:45 -08:00
Anders Melchiorsen c82ca62820 Downgrade limitlessled to 1.0.8 (#12403) 2018-02-13 22:58:49 -08:00
Sean Dague c414ecd4f0 Introduce zone_id to identify player+zone (#12382)
The yamaha component previously used a property named unique_id to
ensure that exactly 1 media_player was discovered per zone per
control_url. This was introduced so that hard coded devices wouldn't
be duplicated by automatically discovered devices.

In HA 0.63 unique_id became a reserved concept as part of the new
device registry, and the property was removed from the component. But
the default returns None, which had the side effect of only ever
registering a single unit + zone, the first one discovered. This was
typically the Main_Zone of the unit, but there is actually no
guaruntee of that.

This fix brings back the logic under a different property called
zone_id. This is not guarunteed to be globally stable like unique_id
is supposed to be, but it is suitable for the deduplication for yamaha
media players.
2018-02-13 22:11:05 -08:00
citruz 72f100723f Updated beacontools (#12368) 2018-02-13 22:11:04 -08:00
Otto Winter 9e4da37022 Fix WUnderground names (#12346)
* 📝 Fix WUnderground names

* 👻 Fix using event loop callback
2018-02-13 22:11:03 -08:00
Rene Nulsch e5f000f976 Fix MercedesMe - add check for unsupported features (#12342)
* Add check for unsupported features

* Lint fix

* change to guard clause
2018-02-13 22:11:03 -08:00
Paulus Schoutsen e2408cc804 Version bump to 0.63.2 2018-02-13 22:03:06 -08:00
Paulus Schoutsen 073126755c Merge pull request #12334 from home-assistant/release-0-63-1
0.63.1
2018-02-12 08:56:04 -08:00
Paulus Schoutsen 7a9ceb6f54 Fix platform dependencies (#12330) 2018-02-11 23:38:47 -08:00
Richard Lucas a06000c76d Always return lockState == LOCKED when handling Alexa.LockController (#12328) 2018-02-11 23:38:39 -08:00
Richard Lucas 2a0bd8d330 Fix Report State for Alexa Brightness Controller (#12318)
* Fix Report State for Alexa Brightness Controller

* Lint
2018-02-11 23:38:31 -08:00
Paulus Schoutsen ead158b68c Respect entity namespace for entity registry (#12313)
* Respect entity namespace for entity registry

* Lint
2018-02-11 23:38:24 -08:00
Paulus Schoutsen bfd9a5a863 Allow overriding name via entity registry (#12292)
* Allow overriding name via entity registry

* Update requirements
2018-02-11 23:38:15 -08:00
Paulus Schoutsen 56b185f7ab Remove unique ID from netatmo (#12317)
* Remove unique ID from netatmo

* Shame platform in error message
2018-02-11 23:31:40 -08:00
Russell Cloran 34ccfae565 zha: Update zigpy-xbee to 0.0.2
0.0.2 implements auto_form, so that configuring the radio to be a
controller is done automatically.
2018-02-11 23:31:30 -08:00
Richard Lucas dc8a0205ee Fix Alexa Step Volume (#12314) 2018-02-11 23:30:55 -08:00
Paulus Schoutsen 7471211b60 Version bump to 0.63.1 2018-02-11 23:27:38 -08:00
29 changed files with 269 additions and 88 deletions
+14 -4
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)
@@ -9,7 +9,7 @@ import datetime
from homeassistant.components.binary_sensor import (BinarySensorDevice)
from homeassistant.components.mercedesme import (
DATA_MME, MercedesMeEntity, BINARY_SENSORS)
DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, BINARY_SENSORS)
DEPENDENCIES = ['mercedesme']
@@ -27,8 +27,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = []
for car in data.cars:
for key, value in sorted(BINARY_SENSORS.items()):
devices.append(MercedesMEBinarySensor(
data, key, value[0], car["vin"], None))
if car['availabilities'].get(key, 'INVALID') == 'VALID':
devices.append(MercedesMEBinarySensor(
data, key, value[0], car["vin"], None))
else:
_LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"])
add_devices(devices, True)
@@ -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."""
@@ -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
@@ -49,10 +49,13 @@ class MercedesMEDeviceTracker(object):
def update_info(self, now=None):
"""Update the device info."""
for device in self.data.cars:
_LOGGER.debug("Updating %s", device["vin"])
if not device['services'].get('VEHICLE_FINDER', False):
continue
location = self.data.get_location(device["vin"])
if location is None:
return False
continue
dev_id = device["vin"]
name = device["license"]
@@ -16,7 +16,7 @@ from homeassistant.components.light import (
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['limitlessled==1.0.9']
REQUIREMENTS = ['limitlessled==1.0.8']
_LOGGER = logging.getLogger(__name__)
@@ -60,7 +60,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
import rxv
# Keep track of configured receivers so that we don't end up
# discovering a receiver dynamically that we have static config
# for. Map each device from its unique_id to an instance since
# for. Map each device from its zone_id to an instance since
# YamahaDevice is not hashable (thus not possible to add to a set).
if hass.data.get(DATA_YAMAHA) is None:
hass.data[DATA_YAMAHA] = {}
@@ -100,8 +100,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
source_names, zone_names)
# Only add device if it's not already added
if device.unique_id not in hass.data[DATA_YAMAHA]:
hass.data[DATA_YAMAHA][device.unique_id] = device
if device.zone_id not in hass.data[DATA_YAMAHA]:
hass.data[DATA_YAMAHA][device.zone_id] = device
devices.append(device)
else:
_LOGGER.debug('Ignoring duplicate receiver %s', name)
@@ -220,6 +220,11 @@ class YamahaDevice(MediaPlayerDevice):
"""List of available input sources."""
return self._source_list
@property
def zone_id(self):
"""Return an zone_id to ensure 1 media player per zone."""
return '{0}:{1}'.format(self.receiver.ctrl_url, self._zone)
@property
def supported_features(self):
"""Flag media player features that are supported."""
+2
View File
@@ -41,6 +41,8 @@ SENSORS = {
DATA_MME = 'mercedesme'
DOMAIN = 'mercedesme'
FEATURE_NOT_AVAILABLE = "The feature %s is not available for your car %s"
NOTIFICATION_ID = 'mercedesme_integration_notification'
NOTIFICATION_TITLE = 'Mercedes me integration setup'
@@ -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
@@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_NAME, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_START)
REQUIREMENTS = ['beacontools[scan]==1.0.1']
REQUIREMENTS = ['beacontools[scan]==1.2.1']
_LOGGER = logging.getLogger(__name__)
@@ -8,7 +8,7 @@ import logging
import datetime
from homeassistant.components.mercedesme import (
DATA_MME, MercedesMeEntity, SENSORS)
DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, SENSORS)
DEPENDENCIES = ['mercedesme']
@@ -29,8 +29,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = []
for car in data.cars:
for key, value in sorted(SENSORS.items()):
devices.append(
MercedesMESensor(data, key, value[0], car["vin"], value[1]))
if car['availabilities'].get(key, 'INVALID') == 'VALID':
devices.append(
MercedesMESensor(
data, key, value[0], car["vin"], value[1]))
else:
_LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"])
add_devices(devices, True)
@@ -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."""
@@ -11,14 +11,14 @@ import re
import requests
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT
from homeassistant.const import (
CONF_MONITORED_CONDITIONS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE,
TEMP_FAHRENHEIT, TEMP_CELSIUS, LENGTH_INCHES, LENGTH_KILOMETERS,
LENGTH_MILES, LENGTH_FEET, STATE_UNKNOWN, ATTR_ATTRIBUTION,
ATTR_FRIENDLY_NAME)
LENGTH_MILES, LENGTH_FEET, STATE_UNKNOWN, ATTR_ATTRIBUTION)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity import Entity, generate_entity_id
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
@@ -637,7 +637,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
config.get(CONF_LANG), latitude, longitude)
sensors = []
for variable in config[CONF_MONITORED_CONDITIONS]:
sensors.append(WUndergroundSensor(rest, variable))
sensors.append(WUndergroundSensor(hass, rest, variable))
rest.update()
if not rest.data:
@@ -651,7 +651,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class WUndergroundSensor(Entity):
"""Implementing the WUnderground sensor."""
def __init__(self, rest, condition):
def __init__(self, hass: HomeAssistantType, rest, condition):
"""Initialize the sensor."""
self.rest = rest
self._condition = condition
@@ -663,6 +663,8 @@ class WUndergroundSensor(Entity):
self._entity_picture = None
self._unit_of_measurement = self._cfg_expand("unit_of_measurement")
self.rest.request_feature(SENSOR_TYPES[condition].feature)
self.entity_id = generate_entity_id(
ENTITY_ID_FORMAT, "pws_" + condition, hass=hass)
def _cfg_expand(self, what, default=None):
"""Parse and return sensor data."""
@@ -684,9 +686,6 @@ class WUndergroundSensor(Entity):
"""Parse and update device state attributes."""
attrs = self._cfg_expand("device_state_attributes", {})
self._attributes[ATTR_FRIENDLY_NAME] = self._cfg_expand(
"friendly_name")
for (attr, callback) in attrs.items():
if callable(callback):
try:
@@ -701,7 +700,7 @@ class WUndergroundSensor(Entity):
@property
def name(self):
"""Return the name of the sensor."""
return "PWS_" + self._condition
return self._cfg_expand("friendly_name")
@property
def state(self):
+1 -1
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'
+1 -1
View File
@@ -2,7 +2,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 63
PATCH_VERSION = '0'
PATCH_VERSION = '2'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 4, 2)
+4 -1
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
+5 -5
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
+10 -1
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)
+22 -8
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
+1
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
+1 -1
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
+4 -3
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
@@ -117,7 +118,7 @@ basicmodem==0.7
batinfo==0.4.2
# homeassistant.components.sensor.eddystone_temperature
# beacontools[scan]==1.0.1
# beacontools[scan]==1.2.1
# homeassistant.components.device_tracker.linksys_ap
# homeassistant.components.sensor.geizhals
@@ -453,7 +454,7 @@ liffylights==0.9.4
lightify==1.0.6.1
# homeassistant.components.light.limitlessled
limitlessled==1.0.9
limitlessled==1.0.8
# homeassistant.components.linode
linode-api==4.1.4b2
@@ -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
+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(
+2 -2
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
+27 -4
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,
+18 -8
View File
@@ -249,31 +249,41 @@ class TestWundergroundSetup(unittest.TestCase):
None)
for device in self.DEVICES:
device.update()
self.assertTrue(str(device.name).startswith('PWS_'))
if device.name == 'PWS_weather':
entity_id = device.entity_id
friendly_name = device.name
self.assertTrue(entity_id.startswith('sensor.pws_'))
if entity_id == 'sensor.pws_weather':
self.assertEqual(HTTPS_ICON_URL, device.entity_picture)
self.assertEqual(WEATHER, device.state)
self.assertIsNone(device.unit_of_measurement)
elif device.name == 'PWS_alerts':
self.assertEqual("Weather Summary", friendly_name)
elif entity_id == 'sensor.pws_alerts':
self.assertEqual(1, device.state)
self.assertEqual(ALERT_MESSAGE,
device.device_state_attributes['Message'])
self.assertEqual(ALERT_ICON, device.icon)
self.assertIsNone(device.entity_picture)
elif device.name == 'PWS_location':
self.assertEqual('Alerts', friendly_name)
elif entity_id == 'sensor.pws_location':
self.assertEqual('Holly Springs, NC', device.state)
elif device.name == 'PWS_elevation':
self.assertEqual('Location', friendly_name)
elif entity_id == 'sensor.pws_elevation':
self.assertEqual('413', device.state)
elif device.name == 'PWS_feelslike_c':
self.assertEqual('Elevation', friendly_name)
elif entity_id == 'sensor.pws_feelslike_c':
self.assertIsNone(device.entity_picture)
self.assertEqual(FEELS_LIKE, device.state)
self.assertEqual(TEMP_CELSIUS, device.unit_of_measurement)
elif device.name == 'PWS_weather_1d_metric':
self.assertEqual("Feels Like", friendly_name)
elif entity_id == 'sensor.pws_weather_1d_metric':
self.assertEqual(FORECAST_TEXT, device.state)
self.assertEqual('Tuesday', friendly_name)
else:
self.assertEqual(device.name, 'PWS_precip_1d_in')
self.assertEqual(entity_id, 'sensor.pws_precip_1d_in')
self.assertEqual(PRECIP_IN, device.state)
self.assertEqual(LENGTH_INCHES, device.unit_of_measurement)
self.assertEqual('Precipitation Intensity Today',
friendly_name)
@unittest.mock.patch('requests.get',
side_effect=ConnectionError('test exception'))
+29 -1
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
+58 -1
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'
+32 -3
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