Merge pull request #12484 from home-assistant/release-0-63-3

0.63.3
This commit is contained in:
Paulus Schoutsen 2018-02-17 15:29:10 -08:00 committed by GitHub
commit 38da81c308
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 68 additions and 71 deletions

View File

@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 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_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity' ATTR_HUMIDITY = 'humidity'

View File

@ -83,9 +83,9 @@ NODE_FILTERS = {
}, },
'fan': { 'fan': {
'uom': [], 'uom': [],
'states': ['on', 'off', 'low', 'medium', 'high'], 'states': ['off', 'low', 'medium', 'high'],
'node_def_id': ['FanLincMotor'], 'node_def_id': ['FanLincMotor'],
'insteon_type': ['1.46.'] 'insteon_type': []
}, },
'cover': { 'cover': {
'uom': ['97'], 'uom': ['97'],
@ -99,7 +99,7 @@ NODE_FILTERS = {
'node_def_id': ['DimmerLampSwitch', 'DimmerLampSwitch_ADV', 'node_def_id': ['DimmerLampSwitch', 'DimmerLampSwitch_ADV',
'DimmerSwitchOnly', 'DimmerSwitchOnly_ADV', 'DimmerSwitchOnly', 'DimmerSwitchOnly_ADV',
'DimmerLampOnly', 'BallastRelayLampSwitch', 'DimmerLampOnly', 'BallastRelayLampSwitch',
'BallastRelayLampSwitch_ADV', 'RelayLampSwitch', 'BallastRelayLampSwitch_ADV',
'RemoteLinc2', 'RemoteLinc2_ADV'], 'RemoteLinc2', 'RemoteLinc2_ADV'],
'insteon_type': ['1.'] 'insteon_type': ['1.']
}, },
@ -431,8 +431,11 @@ class ISYDevice(Entity):
def unique_id(self) -> str: def unique_id(self) -> str:
"""Get the unique identifier of the device.""" """Get the unique identifier of the device."""
# pylint: disable=protected-access # pylint: disable=protected-access
if hasattr(self._node, '_id'):
return self._node._id return self._node._id
return None
@property @property
def name(self) -> str: def name(self) -> str:
"""Get the name of the device.""" """Get the name of the device."""

View File

@ -237,7 +237,6 @@ class LightTemplate(Light):
@asyncio.coroutine @asyncio.coroutine
def async_update(self): def async_update(self):
"""Update the state from the template.""" """Update the state from the template."""
print("ASYNC UPDATE")
if self._template is not None: if self._template is not None:
try: try:
state = self._template.async_render().lower() state = self._template.async_render().lower()
@ -262,7 +261,7 @@ class LightTemplate(Light):
self._state = None self._state = None
if 0 <= int(brightness) <= 255: if 0 <= int(brightness) <= 255:
self._brightness = brightness self._brightness = int(brightness)
else: else:
_LOGGER.error( _LOGGER.error(
'Received invalid brightness : %s' + 'Received invalid brightness : %s' +

View File

@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 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 # The light does not accept cct values < 1
CCT_MIN = 1 CCT_MIN = 1

View File

@ -370,7 +370,8 @@ class PlexClient(MediaPlayerDevice):
self._is_player_available = False self._is_player_available = False
self._media_position = self._session.viewOffset self._media_position = self._session.viewOffset
self._media_content_id = self._session.ratingKey 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() self._set_player_state()

View File

@ -12,8 +12,7 @@ _LOGGER = logging.getLogger(__name__)
def purge_old_data(instance, purge_days): def purge_old_data(instance, purge_days):
"""Purge events and states older than purge_days ago.""" """Purge events and states older than purge_days ago."""
from .models import States, Events from .models import States, Events
from sqlalchemy import orm from sqlalchemy import func
from sqlalchemy.sql import exists
purge_before = dt_util.utcnow() - timedelta(days=purge_days) 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 # 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 # s.t. we can properly restore state even if the entity has not been
# updated in a long time # updated in a long time
states_alias = orm.aliased(States, name='StatesAlias') protected_states = session.query(func.max(States.state_id)) \
protected_states = session.query(States.state_id, States.event_id)\ .group_by(States.entity_id).all()
.filter(~exists()
.where(States.entity_id ==
states_alias.entity_id)
.where(states_alias.last_updated >
States.last_updated))\
.all()
protected_state_ids = tuple((state[0] for state in protected_states)) 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) \ deleted_rows = session.query(States) \
.filter((States.last_updated < purge_before)) \ .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 # Otherwise, if the SQL server has "ON DELETE CASCADE" as default, it
# will delete the protected state when deleting its associated # will delete the protected state when deleting its associated
# event. Also, we would be producing NULLed foreign keys otherwise. # 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) \ deleted_rows = session.query(Events) \
.filter((Events.time_fired < purge_before)) \ .filter((Events.time_fired < purge_before)) \
.filter(~Events.event_id.in_( .filter(~Events.event_id.in_(

View File

@ -21,7 +21,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
REQUIREMENTS = ['python-miio==0.3.5'] REQUIREMENTS = ['python-miio==0.3.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -126,6 +126,8 @@ class SQLSensor(Entity):
except sqlalchemy.exc.SQLAlchemyError as err: except sqlalchemy.exc.SQLAlchemyError as err:
_LOGGER.error("Error executing query %s: %s", self._query, err) _LOGGER.error("Error executing query %s: %s", self._query, err)
return return
finally:
sess.close()
for res in result: for res in result:
_LOGGER.debug(res.items()) _LOGGER.debug(res.items())
@ -141,5 +143,3 @@ class SQLSensor(Entity):
data, None) data, None)
else: else:
self._state = data self._state = data
sess.close()

View File

@ -7,7 +7,6 @@ https://home-assistant.io/components/sensor.yr/
import asyncio import asyncio
import logging import logging
from datetime import timedelta
from random import randrange from random import randrange
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
@ -22,16 +21,17 @@ from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_NAME) ATTR_ATTRIBUTION, CONF_NAME)
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (async_track_utc_time_change,
async_track_point_in_utc_time, async_track_utc_time_change) async_call_later)
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
REQUIREMENTS = ['xmltodict==0.11.0'] REQUIREMENTS = ['xmltodict==0.11.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = "Weather forecast from yr.no, delivered by the Norwegian " \ CONF_ATTRIBUTION = "Weather forecast from met.no, delivered " \
"Meteorological Institute and the NRK." "by the Norwegian Meteorological Institute."
# https://api.met.no/license_data.html
SENSOR_TYPES = { SENSOR_TYPES = {
'symbol': ['Symbol', None], 'symbol': ['Symbol', None],
@ -91,11 +91,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async_add_devices(dev) async_add_devices(dev)
weather = YrData(hass, coordinates, forecast, dev) weather = YrData(hass, coordinates, forecast, dev)
# Update weather on the hour, spread seconds async_track_utc_time_change(hass, weather.updating_devices, minute=31)
async_track_utc_time_change( yield from weather.fetching_data()
hass, weather.async_update, minute=randrange(1, 10),
second=randrange(0, 59))
yield from weather.async_update()
class YrSensor(Entity): class YrSensor(Entity):
@ -153,27 +150,21 @@ class YrData(object):
self._url = 'https://aa015h6buqvih86i1.api.met.no/'\ self._url = 'https://aa015h6buqvih86i1.api.met.no/'\
'weatherapi/locationforecast/1.9/' 'weatherapi/locationforecast/1.9/'
self._urlparams = coordinates self._urlparams = coordinates
self._nextrun = None
self._forecast = forecast self._forecast = forecast
self.devices = devices self.devices = devices
self.data = {} self.data = {}
self.hass = hass self.hass = hass
@asyncio.coroutine @asyncio.coroutine
def async_update(self, *_): def fetching_data(self, *_):
"""Get the latest data from yr.no.""" """Get the latest data from yr.no."""
import xmltodict import xmltodict
def try_again(err: str): def try_again(err: str):
"""Retry in 15 minutes.""" """Retry in 15 to 20 minutes."""
_LOGGER.warning("Retrying in 15 minutes: %s", err) minutes = 15 + randrange(6)
self._nextrun = None _LOGGER.error("Retrying in %i minutes: %s", minutes, err)
nxt = dt_util.utcnow() + timedelta(minutes=15) async_call_later(self.hass, minutes*60, self.fetching_data)
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: try:
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
@ -190,14 +181,19 @@ class YrData(object):
try: try:
self.data = xmltodict.parse(text)['weatherdata'] 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: except (ExpatError, IndexError) as err:
try_again(err) try_again(err)
return 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() now = dt_util.utcnow()
forecast_time = now + dt_util.dt.timedelta(hours=self._forecast) forecast_time = now + dt_util.dt.timedelta(hours=self._forecast)

View File

@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 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_POWER = 'power'
ATTR_TEMPERATURE = 'temperature' ATTR_TEMPERATURE = 'temperature'

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-miio==0.3.5'] REQUIREMENTS = ['python-miio==0.3.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

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

View File

@ -922,7 +922,7 @@ python-juicenet==0.0.5
# homeassistant.components.remote.xiaomi_miio # homeassistant.components.remote.xiaomi_miio
# homeassistant.components.switch.xiaomi_miio # homeassistant.components.switch.xiaomi_miio
# homeassistant.components.vacuum.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio
python-miio==0.3.5 python-miio==0.3.6
# homeassistant.components.media_player.mpd # homeassistant.components.media_player.mpd
python-mpd2==0.5.5 python-mpd2==0.5.5

View File

@ -586,7 +586,7 @@ class TestTemplateLight:
state = self.hass.states.get('light.test_template_light') state = self.hass.states.get('light.test_template_light')
assert state is not None assert state is not None
assert state.attributes.get('brightness') == '42' assert state.attributes.get('brightness') == 42
def test_friendly_name(self): def test_friendly_name(self):
"""Test the accessibility of the friendly_name attribute.""" """Test the accessibility of the friendly_name attribute."""