diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 942aff4ec57..6938926a19b 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -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' diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index d85883e472a..df93bf51250 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -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: diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index d4f2b93e6b5..cfd050f54f2 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -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' + diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index a3c5fa9f62e..f2d327575b1 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -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 diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index b2a89341cf0..dc38bb17dd3 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -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() diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 06bd81c2309..3fffb521d5a 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -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_( diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 32fde57b61a..aa9f0c95a7c 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -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__) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 99da8c3c680..395c082f9d2 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -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() diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 71a4e60e8a6..3f9579c1f13 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -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) diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 87871079a9c..ad71b3944cf 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -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' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index d64f7a754ee..bcfb3b6738e 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -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__) diff --git a/homeassistant/const.py b/homeassistant/const.py index 917292648c9..eb90a19c778 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 63 -PATCH_VERSION = '2' +PATCH_VERSION = '3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) diff --git a/requirements_all.txt b/requirements_all.txt index 6c1282ce8a7..19e6f5cb6e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,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 diff --git a/tests/components/light/test_template.py b/tests/components/light/test_template.py index aaac0617590..2d45ad1bf94 100644 --- a/tests/components/light/test_template.py +++ b/tests/components/light/test_template.py @@ -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."""