Compare commits

...

26 Commits

Author SHA1 Message Date
Paulus Schoutsen 38da81c308 Merge pull request #12484 from home-assistant/release-0-63-3
0.63.3
2018-02-17 15:29:10 -08:00
Diogo Gomes 6ce9b35e81 [SQL Sensor] always close session (#12452)
* close aborted session

* blank line
2018-02-17 13:58:56 -08:00
Anders Melchiorsen bf67d0e650 Optimize recorder purge (#12448) 2018-02-17 13:58:56 -08:00
Andrey fcf97524a2 Fix light template to return brightness as int (#12447) 2018-02-17 13:58:55 -08:00
Ryan McLean b651cdd8f2 Fix for contentRating error (#12445)
* Fix for contentRating

* Use getattr instead of hasattr

* Lint
2018-02-17 13:58:55 -08:00
Daniel Høyer Iversen 6838de3786 Reduce the load on met.no servers, yr.no sensor (#12435)
* Spread the load more for the yr.no sensor

* Spread the load more for the yr.no sensor

* Spread the load more for the yr.no sensor

* Spread the load more for the yr.no sensor

* Spread the load more for the yr.no sensor

* Spread the load more for the yr.no sensor

* Spread the load more for the yr.no sensor

* Spread the load more for the yr.no sensor

* Spread the load more for the yr.no sensor

* fix comment

* Spread the load more for the yr.no sensor

* Spread the load more for the yr.no sensor

* Spread the load more for the yr.no sensor

* Update yr.py
2018-02-17 13:58:54 -08:00
Greg Laabs c9300a98e0 Fixed 3 small issues in isy994 component (#12421)
1. FanLincs have two nodes: one light and one fan motor. In order for each node to get detected as different Hass entity types, I removed the device-type check for FanLinc. The logic will now fall back on the uom checks which should work just fine. (An alternative approach here would be to special case FanLincs and handle them directly - but seeing as the newer 5.x ISY firmware already handles this much better using NodeDefs, I think this quick and dirty approach is fine for the older firmware.) Fixes #12030
2. Some non-dimming switches were appearing as `light`s in Hass due to an duplicate NodeDef being in the light domain filter. Removed! Fixes #12340
3. The `unqiue_id` property was throwing an error for certain entity types that don't have an `_id` property from the ISY. This issue has always been present, but was exposed by the entity registry which seems to be the first thing to actually try reading the `unique_id` property from the isy994 component.
2018-02-17 13:58:53 -08:00
Sebastian Muszynski de7a4b9501 python-miio version bumped. (Closes: #12389, Closes: #12298) (#12392) 2018-02-17 13:58:53 -08:00
Paulus Schoutsen a046e2ed20 Version bump to 0.63.3 2018-02-17 13:39:30 -08:00
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
40 changed files with 336 additions and 158 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"]
+1 -1
View File
@@ -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'
+7 -4
View File
@@ -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__)
+1 -2
View File
@@ -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."""
+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'
+10 -12
View File
@@ -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."""
+2 -2
View File
@@ -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):
+38 -42
View File
@@ -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__)
+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 = '3'
__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
+5 -4
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
@@ -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
+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,
+1 -1
View File
@@ -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."""
+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