Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38da81c308 | |||
| 6ce9b35e81 | |||
| bf67d0e650 | |||
| fcf97524a2 | |||
| b651cdd8f2 | |||
| 6838de3786 | |||
| c9300a98e0 | |||
| de7a4b9501 | |||
| a046e2ed20 | |||
| d85ed8d0fe | |||
| c82ca62820 | |||
| c414ecd4f0 | |||
| 72f100723f | |||
| 9e4da37022 | |||
| e5f000f976 | |||
| e2408cc804 | |||
| 073126755c | |||
| 7a9ceb6f54 | |||
| a06000c76d | |||
| 2a0bd8d330 | |||
| ead158b68c | |||
| bfd9a5a863 | |||
| 56b185f7ab | |||
| 34ccfae565 | |||
| dc8a0205ee | |||
| 7471211b60 |
@@ -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"]
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.5']
|
||||
REQUIREMENTS = ['python-miio==0.3.6']
|
||||
|
||||
ATTR_TEMPERATURE = 'temperature'
|
||||
ATTR_HUMIDITY = 'humidity'
|
||||
|
||||
@@ -83,9 +83,9 @@ NODE_FILTERS = {
|
||||
},
|
||||
'fan': {
|
||||
'uom': [],
|
||||
'states': ['on', 'off', 'low', 'medium', 'high'],
|
||||
'states': ['off', 'low', 'medium', 'high'],
|
||||
'node_def_id': ['FanLincMotor'],
|
||||
'insteon_type': ['1.46.']
|
||||
'insteon_type': []
|
||||
},
|
||||
'cover': {
|
||||
'uom': ['97'],
|
||||
@@ -99,7 +99,7 @@ NODE_FILTERS = {
|
||||
'node_def_id': ['DimmerLampSwitch', 'DimmerLampSwitch_ADV',
|
||||
'DimmerSwitchOnly', 'DimmerSwitchOnly_ADV',
|
||||
'DimmerLampOnly', 'BallastRelayLampSwitch',
|
||||
'BallastRelayLampSwitch_ADV', 'RelayLampSwitch',
|
||||
'BallastRelayLampSwitch_ADV',
|
||||
'RemoteLinc2', 'RemoteLinc2_ADV'],
|
||||
'insteon_type': ['1.']
|
||||
},
|
||||
@@ -431,7 +431,10 @@ class ISYDevice(Entity):
|
||||
def unique_id(self) -> str:
|
||||
"""Get the unique identifier of the device."""
|
||||
# pylint: disable=protected-access
|
||||
return self._node._id
|
||||
if hasattr(self._node, '_id'):
|
||||
return self._node._id
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -237,7 +237,6 @@ class LightTemplate(Light):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the state from the template."""
|
||||
print("ASYNC UPDATE")
|
||||
if self._template is not None:
|
||||
try:
|
||||
state = self._template.async_render().lower()
|
||||
@@ -262,7 +261,7 @@ class LightTemplate(Light):
|
||||
self._state = None
|
||||
|
||||
if 0 <= int(brightness) <= 255:
|
||||
self._brightness = brightness
|
||||
self._brightness = int(brightness)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
'Received invalid brightness : %s' +
|
||||
|
||||
@@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.5']
|
||||
REQUIREMENTS = ['python-miio==0.3.6']
|
||||
|
||||
# The light does not accept cct values < 1
|
||||
CCT_MIN = 1
|
||||
|
||||
@@ -370,7 +370,8 @@ class PlexClient(MediaPlayerDevice):
|
||||
self._is_player_available = False
|
||||
self._media_position = self._session.viewOffset
|
||||
self._media_content_id = self._session.ratingKey
|
||||
self._media_content_rating = self._session.contentRating
|
||||
self._media_content_rating = getattr(
|
||||
self._session, 'contentRating', None)
|
||||
|
||||
self._set_player_state()
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def purge_old_data(instance, purge_days):
|
||||
"""Purge events and states older than purge_days ago."""
|
||||
from .models import States, Events
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.sql import exists
|
||||
from sqlalchemy import func
|
||||
|
||||
purge_before = dt_util.utcnow() - timedelta(days=purge_days)
|
||||
|
||||
@@ -21,18 +20,10 @@ def purge_old_data(instance, purge_days):
|
||||
# For each entity, the most recent state is protected from deletion
|
||||
# s.t. we can properly restore state even if the entity has not been
|
||||
# updated in a long time
|
||||
states_alias = orm.aliased(States, name='StatesAlias')
|
||||
protected_states = session.query(States.state_id, States.event_id)\
|
||||
.filter(~exists()
|
||||
.where(States.entity_id ==
|
||||
states_alias.entity_id)
|
||||
.where(states_alias.last_updated >
|
||||
States.last_updated))\
|
||||
.all()
|
||||
protected_states = session.query(func.max(States.state_id)) \
|
||||
.group_by(States.entity_id).all()
|
||||
|
||||
protected_state_ids = tuple((state[0] for state in protected_states))
|
||||
protected_event_ids = tuple((state[1] for state in protected_states
|
||||
if state[1] is not None))
|
||||
|
||||
deleted_rows = session.query(States) \
|
||||
.filter((States.last_updated < purge_before)) \
|
||||
@@ -45,6 +36,13 @@ def purge_old_data(instance, purge_days):
|
||||
# Otherwise, if the SQL server has "ON DELETE CASCADE" as default, it
|
||||
# will delete the protected state when deleting its associated
|
||||
# event. Also, we would be producing NULLed foreign keys otherwise.
|
||||
protected_events = session.query(States.event_id) \
|
||||
.filter(States.state_id.in_(protected_state_ids)) \
|
||||
.filter(States.event_id.isnot(None)) \
|
||||
.all()
|
||||
|
||||
protected_event_ids = tuple((state[0] for state in protected_events))
|
||||
|
||||
deleted_rows = session.query(Events) \
|
||||
.filter((Events.time_fired < purge_before)) \
|
||||
.filter(~Events.event_id.in_(
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.const import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.5']
|
||||
REQUIREMENTS = ['python-miio==0.3.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -126,6 +126,8 @@ class SQLSensor(Entity):
|
||||
except sqlalchemy.exc.SQLAlchemyError as err:
|
||||
_LOGGER.error("Error executing query %s: %s", self._query, err)
|
||||
return
|
||||
finally:
|
||||
sess.close()
|
||||
|
||||
for res in result:
|
||||
_LOGGER.debug(res.items())
|
||||
@@ -141,5 +143,3 @@ class SQLSensor(Entity):
|
||||
data, None)
|
||||
else:
|
||||
self._state = data
|
||||
|
||||
sess.close()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -7,7 +7,6 @@ https://home-assistant.io/components/sensor.yr/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
from random import randrange
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
@@ -22,16 +21,17 @@ from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, CONF_NAME)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_point_in_utc_time, async_track_utc_time_change)
|
||||
from homeassistant.helpers.event import (async_track_utc_time_change,
|
||||
async_call_later)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['xmltodict==0.11.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ATTRIBUTION = "Weather forecast from yr.no, delivered by the Norwegian " \
|
||||
"Meteorological Institute and the NRK."
|
||||
CONF_ATTRIBUTION = "Weather forecast from met.no, delivered " \
|
||||
"by the Norwegian Meteorological Institute."
|
||||
# https://api.met.no/license_data.html
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'symbol': ['Symbol', None],
|
||||
@@ -91,11 +91,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async_add_devices(dev)
|
||||
|
||||
weather = YrData(hass, coordinates, forecast, dev)
|
||||
# Update weather on the hour, spread seconds
|
||||
async_track_utc_time_change(
|
||||
hass, weather.async_update, minute=randrange(1, 10),
|
||||
second=randrange(0, 59))
|
||||
yield from weather.async_update()
|
||||
async_track_utc_time_change(hass, weather.updating_devices, minute=31)
|
||||
yield from weather.fetching_data()
|
||||
|
||||
|
||||
class YrSensor(Entity):
|
||||
@@ -153,50 +150,49 @@ class YrData(object):
|
||||
self._url = 'https://aa015h6buqvih86i1.api.met.no/'\
|
||||
'weatherapi/locationforecast/1.9/'
|
||||
self._urlparams = coordinates
|
||||
self._nextrun = None
|
||||
self._forecast = forecast
|
||||
self.devices = devices
|
||||
self.data = {}
|
||||
self.hass = hass
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self, *_):
|
||||
def fetching_data(self, *_):
|
||||
"""Get the latest data from yr.no."""
|
||||
import xmltodict
|
||||
|
||||
def try_again(err: str):
|
||||
"""Retry in 15 minutes."""
|
||||
_LOGGER.warning("Retrying in 15 minutes: %s", err)
|
||||
self._nextrun = None
|
||||
nxt = dt_util.utcnow() + timedelta(minutes=15)
|
||||
if nxt.minute >= 15:
|
||||
async_track_point_in_utc_time(self.hass, self.async_update,
|
||||
nxt)
|
||||
|
||||
if self._nextrun is None or dt_util.utcnow() >= self._nextrun:
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
resp = yield from websession.get(
|
||||
self._url, params=self._urlparams)
|
||||
if resp.status != 200:
|
||||
try_again('{} returned {}'.format(resp.url, resp.status))
|
||||
return
|
||||
text = yield from resp.text()
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
|
||||
try_again(err)
|
||||
"""Retry in 15 to 20 minutes."""
|
||||
minutes = 15 + randrange(6)
|
||||
_LOGGER.error("Retrying in %i minutes: %s", minutes, err)
|
||||
async_call_later(self.hass, minutes*60, self.fetching_data)
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
resp = yield from websession.get(
|
||||
self._url, params=self._urlparams)
|
||||
if resp.status != 200:
|
||||
try_again('{} returned {}'.format(resp.url, resp.status))
|
||||
return
|
||||
text = yield from resp.text()
|
||||
|
||||
try:
|
||||
self.data = xmltodict.parse(text)['weatherdata']
|
||||
model = self.data['meta']['model']
|
||||
if '@nextrun' not in model:
|
||||
model = model[0]
|
||||
self._nextrun = dt_util.parse_datetime(model['@nextrun'])
|
||||
except (ExpatError, IndexError) as err:
|
||||
try_again(err)
|
||||
return
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
|
||||
try_again(err)
|
||||
return
|
||||
|
||||
try:
|
||||
self.data = xmltodict.parse(text)['weatherdata']
|
||||
except (ExpatError, IndexError) as err:
|
||||
try_again(err)
|
||||
return
|
||||
|
||||
yield from self.updating_devices()
|
||||
async_call_later(self.hass, 60*60, self.fetching_data)
|
||||
|
||||
@asyncio.coroutine
|
||||
def updating_devices(self, *_):
|
||||
"""Find the current data from self.data."""
|
||||
if not self.data:
|
||||
return
|
||||
|
||||
now = dt_util.utcnow()
|
||||
forecast_time = now + dt_util.dt.timedelta(hours=self._forecast)
|
||||
|
||||
@@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.5']
|
||||
REQUIREMENTS = ['python-miio==0.3.6']
|
||||
|
||||
ATTR_POWER = 'power'
|
||||
ATTR_TEMPERATURE = 'temperature'
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.5']
|
||||
REQUIREMENTS = ['python-miio==0.3.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 63
|
||||
PATCH_VERSION = '0'
|
||||
PATCH_VERSION = '3'
|
||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||
REQUIRED_PYTHON_VER = (3, 4, 2)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -921,7 +922,7 @@ python-juicenet==0.0.5
|
||||
# homeassistant.components.remote.xiaomi_miio
|
||||
# homeassistant.components.switch.xiaomi_miio
|
||||
# homeassistant.components.vacuum.xiaomi_miio
|
||||
python-miio==0.3.5
|
||||
python-miio==0.3.6
|
||||
|
||||
# homeassistant.components.media_player.mpd
|
||||
python-mpd2==0.5.5
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -586,7 +586,7 @@ class TestTemplateLight:
|
||||
state = self.hass.states.get('light.test_template_light')
|
||||
assert state is not None
|
||||
|
||||
assert state.attributes.get('brightness') == '42'
|
||||
assert state.attributes.get('brightness') == 42
|
||||
|
||||
def test_friendly_name(self):
|
||||
"""Test the accessibility of the friendly_name attribute."""
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user