Merge pull request #13033 from home-assistant/release-0-65-1

0.65.1
This commit is contained in:
Paulus Schoutsen 2018-03-09 20:17:02 -08:00 committed by GitHub
commit 905d71c9e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 140 additions and 65 deletions

View File

@ -113,7 +113,10 @@ def async_from_config_dict(config: Dict[str, Any],
yield from hass.async_add_job(loader.prepare, hass) yield from hass.async_add_job(loader.prepare, hass)
# Make a copy because we are mutating it. # Make a copy because we are mutating it.
config = OrderedDict(config) new_config = OrderedDict()
for key, value in config.items():
new_config[key] = value or {}
config = new_config
# Merge packages # Merge packages
conf_util.merge_packages_config( conf_util.merge_packages_config(

View File

@ -111,6 +111,9 @@ SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema(vol.All(
ATTR_ATTRIBUTES: dict, ATTR_ATTRIBUTES: dict,
ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES), ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES),
ATTR_CONSIDER_HOME: cv.time_period, ATTR_CONSIDER_HOME: cv.time_period,
# Temp workaround for iOS app introduced in 0.65
vol.Optional('battery_status'): str,
vol.Optional('hostname'): str,
})) }))
@ -219,7 +222,11 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
@asyncio.coroutine @asyncio.coroutine
def async_see_service(call): def async_see_service(call):
"""Service to see a device.""" """Service to see a device."""
yield from tracker.async_see(**call.data) # Temp workaround for iOS, introduced in 0.65
data = dict(call.data)
data.pop('hostname', None)
data.pop('battery_status', None)
yield from tracker.async_see(**data)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA) DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA)

View File

@ -5,6 +5,7 @@ from homeassistant.components import (
cover, cover,
group, group,
fan, fan,
input_boolean,
media_player, media_player,
light, light,
scene, scene,
@ -182,6 +183,7 @@ class OnOffTrait(_Trait):
"""Test if state is supported.""" """Test if state is supported."""
return domain in ( return domain in (
group.DOMAIN, group.DOMAIN,
input_boolean.DOMAIN,
switch.DOMAIN, switch.DOMAIN,
fan.DOMAIN, fan.DOMAIN,
light.DOMAIN, light.DOMAIN,

View File

@ -5,16 +5,17 @@ from pyhap.accessory import Accessory, Bridge, Category
from .const import ( from .const import (
SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER,
CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER) CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def set_accessory_info(acc, model, manufacturer=MANUFACTURER, def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
serial_number='0000'): serial_number='0000'):
"""Set the default accessory information.""" """Set the default accessory information."""
service = acc.get_service(SERV_ACCESSORY_INFO) service = acc.get_service(SERV_ACCESSORY_INFO)
service.get_characteristic(CHAR_NAME).set_value(name)
service.get_characteristic(CHAR_MODEL).set_value(model) service.get_characteristic(CHAR_MODEL).set_value(model)
service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer) service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer)
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
@ -49,7 +50,7 @@ class HomeAccessory(Accessory):
def __init__(self, display_name, model, category='OTHER', **kwargs): def __init__(self, display_name, model, category='OTHER', **kwargs):
"""Initialize a Accessory object.""" """Initialize a Accessory object."""
super().__init__(display_name, **kwargs) super().__init__(display_name, **kwargs)
set_accessory_info(self, model) set_accessory_info(self, display_name, model)
self.category = getattr(Category, category, Category.OTHER) self.category = getattr(Category, category, Category.OTHER)
def _set_services(self): def _set_services(self):
@ -62,7 +63,7 @@ class HomeBridge(Bridge):
def __init__(self, display_name, model, pincode, **kwargs): def __init__(self, display_name, model, pincode, **kwargs):
"""Initialize a Bridge object.""" """Initialize a Bridge object."""
super().__init__(display_name, pincode=pincode, **kwargs) super().__init__(display_name, pincode=pincode, **kwargs)
set_accessory_info(self, model) set_accessory_info(self, display_name, model)
def _set_services(self): def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO) add_preload_service(self, SERV_ACCESSORY_INFO)

View File

@ -22,6 +22,7 @@ CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_LINK_QUALITY = 'LinkQuality' CHAR_LINK_QUALITY = 'LinkQuality'
CHAR_MANUFACTURER = 'Manufacturer' CHAR_MANUFACTURER = 'Manufacturer'
CHAR_MODEL = 'Model' CHAR_MODEL = 'Model'
CHAR_NAME = 'Name'
CHAR_ON = 'On' CHAR_ON = 'On'
CHAR_POSITION_STATE = 'PositionState' CHAR_POSITION_STATE = 'PositionState'
CHAR_REACHABLE = 'Reachable' CHAR_REACHABLE = 'Reachable'

View File

@ -423,19 +423,17 @@ class BluesoundPlayer(MediaPlayerDevice):
for player in self._hass.data[DATA_BLUESOUND]: for player in self._hass.data[DATA_BLUESOUND]:
yield from player.force_update_sync_status() yield from player.force_update_sync_status()
@asyncio.coroutine
@Throttle(SYNC_STATUS_INTERVAL) @Throttle(SYNC_STATUS_INTERVAL)
def async_update_sync_status(self, on_updated_cb=None, async def async_update_sync_status(self, on_updated_cb=None,
raise_timeout=False): raise_timeout=False):
"""Update sync status.""" """Update sync status."""
yield from self.force_update_sync_status( await self.force_update_sync_status(
on_updated_cb, raise_timeout=False) on_updated_cb, raise_timeout=False)
@asyncio.coroutine
@Throttle(UPDATE_CAPTURE_INTERVAL) @Throttle(UPDATE_CAPTURE_INTERVAL)
def async_update_captures(self): async def async_update_captures(self):
"""Update Capture sources.""" """Update Capture sources."""
resp = yield from self.send_bluesound_command( resp = await self.send_bluesound_command(
'RadioBrowse?service=Capture') 'RadioBrowse?service=Capture')
if not resp: if not resp:
return return
@ -459,11 +457,10 @@ class BluesoundPlayer(MediaPlayerDevice):
return self._capture_items return self._capture_items
@asyncio.coroutine
@Throttle(UPDATE_PRESETS_INTERVAL) @Throttle(UPDATE_PRESETS_INTERVAL)
def async_update_presets(self): async def async_update_presets(self):
"""Update Presets.""" """Update Presets."""
resp = yield from self.send_bluesound_command('Presets') resp = await self.send_bluesound_command('Presets')
if not resp: if not resp:
return return
self._preset_items = [] self._preset_items = []
@ -488,11 +485,10 @@ class BluesoundPlayer(MediaPlayerDevice):
return self._preset_items return self._preset_items
@asyncio.coroutine
@Throttle(UPDATE_SERVICES_INTERVAL) @Throttle(UPDATE_SERVICES_INTERVAL)
def async_update_services(self): async def async_update_services(self):
"""Update Services.""" """Update Services."""
resp = yield from self.send_bluesound_command('Services') resp = await self.send_bluesound_command('Services')
if not resp: if not resp:
return return
self._services_items = [] self._services_items = []

View File

@ -253,8 +253,7 @@ class Volumio(MediaPlayerDevice):
return self.send_volumio_msg('commands', return self.send_volumio_msg('commands',
params={'cmd': 'clearQueue'}) params={'cmd': 'clearQueue'})
@asyncio.coroutine
@Throttle(PLAYLIST_UPDATE_INTERVAL) @Throttle(PLAYLIST_UPDATE_INTERVAL)
def _async_update_playlists(self, **kwargs): async def _async_update_playlists(self, **kwargs):
"""Update available Volumio playlists.""" """Update available Volumio playlists."""
self._playlists = yield from self.send_volumio_msg('listplaylists') self._playlists = await self.send_volumio_msg('listplaylists')

View File

@ -157,13 +157,12 @@ class FidoData(object):
REQUESTS_TIMEOUT, httpsession) REQUESTS_TIMEOUT, httpsession)
self.data = {} self.data = {}
@asyncio.coroutine
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def async_update(self): async def async_update(self):
"""Get the latest data from Fido.""" """Get the latest data from Fido."""
from pyfido.client import PyFidoError from pyfido.client import PyFidoError
try: try:
yield from self.client.fetch_data() await self.client.fetch_data()
except PyFidoError as exp: except PyFidoError as exp:
_LOGGER.error("Error on receive last Fido data: %s", exp) _LOGGER.error("Error on receive last Fido data: %s", exp)
return False return False

View File

@ -182,13 +182,12 @@ class HydroquebecData(object):
return self.client.get_contracts() return self.client.get_contracts()
return [] return []
@asyncio.coroutine
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def _fetch_data(self): async def _fetch_data(self):
"""Fetch latest data from HydroQuebec.""" """Fetch latest data from HydroQuebec."""
from pyhydroquebec.client import PyHydroQuebecError from pyhydroquebec.client import PyHydroQuebecError
try: try:
yield from self.client.fetch_data() await self.client.fetch_data()
except PyHydroQuebecError as exp: except PyHydroQuebecError as exp:
_LOGGER.error("Error on receive last Hydroquebec data: %s", exp) _LOGGER.error("Error on receive last Hydroquebec data: %s", exp)
return False return False

View File

@ -133,13 +133,9 @@ class LuftdatenSensor(Entity):
except KeyError: except KeyError:
return return
@asyncio.coroutine async def async_update(self):
def async_update(self):
"""Get the latest data from luftdaten.info and update the state.""" """Get the latest data from luftdaten.info and update the state."""
try: await self.luftdaten.async_update()
yield from self.luftdaten.async_update()
except TypeError:
pass
class LuftdatenData(object): class LuftdatenData(object):
@ -150,12 +146,11 @@ class LuftdatenData(object):
self.data = data self.data = data
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
@asyncio.coroutine async def async_update(self):
def async_update(self):
"""Get the latest data from luftdaten.info.""" """Get the latest data from luftdaten.info."""
from luftdaten.exceptions import LuftdatenError from luftdaten.exceptions import LuftdatenError
try: try:
yield from self.data.async_get_data() await self.data.async_get_data()
except LuftdatenError: except LuftdatenError:
_LOGGER.error("Unable to retrieve data from luftdaten.info") _LOGGER.error("Unable to retrieve data from luftdaten.info")

View File

@ -75,15 +75,14 @@ def setup_sabnzbd(base_url, apikey, name, config,
for variable in monitored]) for variable in monitored])
@asyncio.coroutine
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def async_update_queue(sab_api): async def async_update_queue(sab_api):
""" """
Throttled function to update SABnzbd queue. Throttled function to update SABnzbd queue.
This ensures that the queue info only gets updated once for all sensors This ensures that the queue info only gets updated once for all sensors
""" """
yield from sab_api.refresh_queue() await sab_api.refresh_queue()
def request_configuration(host, name, hass, config, async_add_devices, def request_configuration(host, name, hass, config, async_add_devices,

View File

@ -140,21 +140,20 @@ class StartcaData(object):
""" """
return float(value) * 10 ** -9 return float(value) * 10 ** -9
@asyncio.coroutine
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def async_update(self): async def async_update(self):
"""Get the Start.ca bandwidth data from the web service.""" """Get the Start.ca bandwidth data from the web service."""
import xmltodict import xmltodict
_LOGGER.debug("Updating Start.ca usage data") _LOGGER.debug("Updating Start.ca usage data")
url = 'https://www.start.ca/support/usage/api?key=' + \ url = 'https://www.start.ca/support/usage/api?key=' + \
self.api_key self.api_key
with async_timeout.timeout(REQUEST_TIMEOUT, loop=self.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=self.loop):
req = yield from self.websession.get(url) req = await self.websession.get(url)
if req.status != 200: if req.status != 200:
_LOGGER.error("Request failed with status: %u", req.status) _LOGGER.error("Request failed with status: %u", req.status)
return False return False
data = yield from req.text() data = await req.text()
try: try:
xml_data = xmltodict.parse(data) xml_data = xmltodict.parse(data)
except ExpatError: except ExpatError:

View File

@ -132,22 +132,21 @@ class TekSavvyData(object):
self.data = {"limit": self.bandwidth_cap} if self.bandwidth_cap > 0 \ self.data = {"limit": self.bandwidth_cap} if self.bandwidth_cap > 0 \
else {"limit": float('inf')} else {"limit": float('inf')}
@asyncio.coroutine
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def async_update(self): async def async_update(self):
"""Get the TekSavvy bandwidth data from the web service.""" """Get the TekSavvy bandwidth data from the web service."""
headers = {"TekSavvy-APIKey": self.api_key} headers = {"TekSavvy-APIKey": self.api_key}
_LOGGER.debug("Updating TekSavvy data") _LOGGER.debug("Updating TekSavvy data")
url = "https://api.teksavvy.com/"\ url = "https://api.teksavvy.com/"\
"web/Usage/UsageSummaryRecords?$filter=IsCurrent%20eq%20true" "web/Usage/UsageSummaryRecords?$filter=IsCurrent%20eq%20true"
with async_timeout.timeout(REQUEST_TIMEOUT, loop=self.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=self.loop):
req = yield from self.websession.get(url, headers=headers) req = await self.websession.get(url, headers=headers)
if req.status != 200: if req.status != 200:
_LOGGER.error("Request failed with status: %u", req.status) _LOGGER.error("Request failed with status: %u", req.status)
return False return False
try: try:
data = yield from req.json() data = await req.json()
for (api, ha_name) in API_HA_MAP: for (api, ha_name) in API_HA_MAP:
self.data[ha_name] = float(data["value"][0][api]) self.data[ha_name] = float(data["value"][0][api])
on_peak_download = self.data["onpeak_download"] on_peak_download = self.data["onpeak_download"]

View File

@ -777,14 +777,13 @@ class WUndergroundData(object):
return url + '.json' return url + '.json'
@asyncio.coroutine
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def async_update(self): async def async_update(self):
"""Get the latest data from WUnderground.""" """Get the latest data from WUnderground."""
try: try:
with async_timeout.timeout(10, loop=self._hass.loop): with async_timeout.timeout(10, loop=self._hass.loop):
response = yield from self._session.get(self._build_url()) response = await self._session.get(self._build_url())
result = yield from response.json() result = await response.json()
if "error" in result['response']: if "error" in result['response']:
raise ValueError(result['response']["error"]["description"]) raise ValueError(result['response']["error"]["description"])
self.data = result self.data = result

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 = 65 MINOR_VERSION = 65
PATCH_VERSION = '0' PATCH_VERSION = '1'
__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, 5, 3) REQUIRED_PYTHON_VER = (3, 5, 3)

View File

@ -1,4 +1,5 @@
"""Helper methods for various modules.""" """Helper methods for various modules."""
import asyncio
from collections.abc import MutableSet from collections.abc import MutableSet
from itertools import chain from itertools import chain
import threading import threading
@ -276,6 +277,16 @@ class Throttle(object):
is_func = (not hasattr(method, '__self__') and is_func = (not hasattr(method, '__self__') and
'.' not in method.__qualname__.split('.<locals>.')[-1]) '.' not in method.__qualname__.split('.<locals>.')[-1])
# Make sure we return a coroutine if the method is async.
if asyncio.iscoroutinefunction(method):
async def throttled_value():
"""Stand-in function for when real func is being throttled."""
return None
else:
def throttled_value():
"""Stand-in function for when real func is being throttled."""
return None
@wraps(method) @wraps(method)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
"""Wrap that allows wrapped to be called only once per min_time. """Wrap that allows wrapped to be called only once per min_time.
@ -298,7 +309,7 @@ class Throttle(object):
throttle = host._throttle[id(self)] throttle = host._throttle[id(self)]
if not throttle[0].acquire(False): if not throttle[0].acquire(False):
return None return throttled_value()
# Check if method is never called or no_throttle is given # Check if method is never called or no_throttle is given
force = kwargs.pop('no_throttle', False) or not throttle[1] force = kwargs.pop('no_throttle', False) or not throttle[1]
@ -309,7 +320,7 @@ class Throttle(object):
throttle[1] = utcnow() throttle[1] = utcnow()
return result return result
return None return throttled_value()
finally: finally:
throttle[0].release() throttle[0].release()

View File

@ -730,3 +730,18 @@ async def test_old_style_track_new_is_skipped(mock_device_tracker_conf, hass):
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_device_tracker_conf) == 1 assert len(mock_device_tracker_conf) == 1
assert mock_device_tracker_conf[0].track is False assert mock_device_tracker_conf[0].track is False
def test_see_schema_allowing_ios_calls():
"""Test SEE service schema allows extra keys.
Temp work around because the iOS app sends incorrect data.
"""
device_tracker.SERVICE_SEE_PAYLOAD_SCHEMA({
'dev_id': 'Test',
"battery": 35,
"battery_status": 'Unplugged',
"gps": [10.0, 10.0],
"gps_accuracy": 300,
"hostname": 'beer',
})

View File

@ -9,8 +9,9 @@ from homeassistant.components import (
climate, climate,
cover, cover,
fan, fan,
media_player, input_boolean,
light, light,
media_player,
scene, scene,
script, script,
switch, switch,
@ -138,6 +139,43 @@ async def test_onoff_group(hass):
} }
async def test_onoff_input_boolean(hass):
"""Test OnOff trait support for input_boolean domain."""
assert trait.OnOffTrait.supported(media_player.DOMAIN, 0)
trt_on = trait.OnOffTrait(State('input_boolean.bla', STATE_ON))
assert trt_on.sync_attributes() == {}
assert trt_on.query_attributes() == {
'on': True
}
trt_off = trait.OnOffTrait(State('input_boolean.bla', STATE_OFF))
assert trt_off.query_attributes() == {
'on': False
}
on_calls = async_mock_service(hass, input_boolean.DOMAIN, SERVICE_TURN_ON)
await trt_on.execute(hass, trait.COMMAND_ONOFF, {
'on': True
})
assert len(on_calls) == 1
assert on_calls[0].data == {
ATTR_ENTITY_ID: 'input_boolean.bla',
}
off_calls = async_mock_service(hass, input_boolean.DOMAIN,
SERVICE_TURN_OFF)
await trt_on.execute(hass, trait.COMMAND_ONOFF, {
'on': False
})
assert len(off_calls) == 1
assert off_calls[0].data == {
ATTR_ENTITY_ID: 'input_boolean.bla',
}
async def test_onoff_switch(hass): async def test_onoff_switch(hass):
"""Test OnOff trait support for switch domain.""" """Test OnOff trait support for switch domain."""
assert trait.OnOffTrait.supported(media_player.DOMAIN, 0) assert trait.OnOffTrait.supported(media_player.DOMAIN, 0)

View File

@ -13,7 +13,7 @@ from homeassistant.components.homekit.accessories import (
HomeAccessory, HomeBridge) HomeAccessory, HomeBridge)
from homeassistant.components.homekit.const import ( from homeassistant.components.homekit.const import (
SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE,
CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER) CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER)
from tests.mock.homekit import ( from tests.mock.homekit import (
get_patch_paths, mock_preload_service, get_patch_paths, mock_preload_service,
@ -69,21 +69,23 @@ def test_override_properties():
def test_set_accessory_info(): def test_set_accessory_info():
"""Test setting of basic accessory information with MockAccessory.""" """Test setting of basic accessory information with MockAccessory."""
acc = MockAccessory('Accessory') acc = MockAccessory('Accessory')
set_accessory_info(acc, 'model', 'manufacturer', '0000') set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000')
assert len(acc.services) == 1 assert len(acc.services) == 1
serv = acc.services[0] serv = acc.services[0]
assert serv.display_name == SERV_ACCESSORY_INFO assert serv.display_name == SERV_ACCESSORY_INFO
assert len(serv.characteristics) == 3 assert len(serv.characteristics) == 4
chars = serv.characteristics chars = serv.characteristics
assert chars[0].display_name == CHAR_MODEL assert chars[0].display_name == CHAR_NAME
assert chars[0].value == 'model' assert chars[0].value == 'name'
assert chars[1].display_name == CHAR_MANUFACTURER assert chars[1].display_name == CHAR_MODEL
assert chars[1].value == 'manufacturer' assert chars[1].value == 'model'
assert chars[2].display_name == CHAR_SERIAL_NUMBER assert chars[2].display_name == CHAR_MANUFACTURER
assert chars[2].value == '0000' assert chars[2].value == 'manufacturer'
assert chars[3].display_name == CHAR_SERIAL_NUMBER
assert chars[3].value == '0000'
@patch(PATH_ACC, side_effect=mock_preload_service) @patch(PATH_ACC, side_effect=mock_preload_service)

View File

@ -280,3 +280,14 @@ class TestUtil(unittest.TestCase):
mock_random.SystemRandom.return_value = generator mock_random.SystemRandom.return_value = generator
assert util.get_random_string(length=3) == 'ABC' assert util.get_random_string(length=3) == 'ABC'
async def test_throttle_async():
"""Test Throttle decorator with async method."""
@util.Throttle(timedelta(seconds=2))
async def test_method():
"""Only first call should return a value."""
return True
assert (await test_method()) is True
assert (await test_method()) is None