Merge pull request #5032 from home-assistant/release-0-35-3

0.35.3
This commit is contained in:
Paulus Schoutsen 2016-12-23 07:05:54 +01:00 committed by GitHub
commit 308d71c448
15 changed files with 107 additions and 73 deletions

View File

@ -115,7 +115,7 @@ def async_setup(hass, config):
update_coro = hass.loop.create_task(
alarm.async_update_ha_state(True))
if hasattr(alarm, 'async_update'):
update_tasks.append(hass.loop.create_task(update_coro))
update_tasks.append(update_coro)
else:
yield from update_coro

View File

@ -13,8 +13,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.sensor.nest import NestSensor
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS)
from homeassistant.components.nest import (
DATA_NEST, is_thermostat, is_camera)
from homeassistant.components.nest import DATA_NEST
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['nest']
@ -76,9 +75,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error(wstr)
sensors = []
device_chain = chain(nest.devices(),
nest.protect_devices(),
nest.camera_devices())
device_chain = chain(nest.thermostats(),
nest.smoke_co_alarms(),
nest.cameras())
for structure, device in device_chain:
sensors += [NestBinarySensor(structure, device, variable)
for variable in conf
@ -86,9 +85,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensors += [NestBinarySensor(structure, device, variable)
for variable in conf
if variable in CLIMATE_BINARY_TYPES
and is_thermostat(device)]
and device.is_thermostat]
if is_camera(device):
if device.is_camera:
sensors += [NestBinarySensor(structure, device, variable)
for variable in conf
if variable in CAMERA_BINARY_TYPES]
@ -118,13 +117,14 @@ class NestActivityZoneSensor(NestBinarySensor):
def __init__(self, structure, device, zone):
"""Initialize the sensor."""
super(NestActivityZoneSensor, self).__init__(structure, device, None)
super(NestActivityZoneSensor, self).__init__(structure, device, "")
self.zone = zone
self._name = "{} {} activity".format(self._name, self.zone.name)
@property
def name(self):
"""Return the name of the nest, if any."""
return "{} {} activity".format(self._name, self.zone.name)
return self._name
def update(self):
"""Retrieve latest state."""

View File

@ -27,7 +27,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None:
return
camera_devices = hass.data[nest.DATA_NEST].camera_devices()
camera_devices = hass.data[nest.DATA_NEST].cameras()
cameras = [NestCamera(structure, device)
for structure, device in camera_devices]
add_devices(cameras, True)
@ -43,7 +43,7 @@ class NestCamera(Camera):
self.device = device
self._location = None
self._name = None
self._is_online = None
self._online = None
self._is_streaming = None
self._is_video_history_enabled = False
# Default to non-NestAware subscribed, but will be fixed during update
@ -76,7 +76,7 @@ class NestCamera(Camera):
"""Cache value from Python-nest."""
self._location = self.device.where
self._name = self.device.name
self._is_online = self.device.is_online
self._online = self.device.online
self._is_streaming = self.device.is_streaming
self._is_video_history_enabled = self.device.is_video_history_enabled

View File

@ -40,7 +40,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(
[NestThermostat(structure, device, temp_unit)
for structure, device in hass.data[DATA_NEST].devices()],
for structure, device in hass.data[DATA_NEST].thermostats()],
True
)

View File

@ -253,7 +253,7 @@ def async_setup(hass, config):
update_coro = hass.loop.create_task(
light.async_update_ha_state(True))
if hasattr(light, 'async_update'):
update_tasks.append(hass.loop.create_task(update_coro))
update_tasks.append(update_coro)
else:
yield from update_coro

View File

@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = [
'http://github.com/technicalpickles/python-nest'
'/archive/b8391d2b3cb8682f8b0c2bdff477179983609f39.zip' # nest-cam branch
'#python-nest==3.0.2']
'/archive/e6c9d56a8df455d4d7746389811f2c1387e8cb33.zip' # nest-cam branch
'#python-nest==3.0.3']
DOMAIN = 'nest'
@ -132,12 +132,12 @@ class NestDevice(object):
self._structure = conf[CONF_STRUCTURE]
_LOGGER.debug("Structures to include: %s", self._structure)
def devices(self):
"""Generator returning list of devices and their location."""
def thermostats(self):
"""Generator returning list of thermostats and their location."""
try:
for structure in self.nest.structures:
if structure.name in self._structure:
for device in structure.devices:
for device in structure.thermostats:
yield (structure, device)
else:
_LOGGER.debug("Ignoring structure %s, not in %s",
@ -146,12 +146,12 @@ class NestDevice(object):
_LOGGER.error(
"Connection error logging into the nest web service.")
def protect_devices(self):
"""Generator returning list of protect devices."""
def smoke_co_alarms(self):
"""Generator returning list of smoke co alarams."""
try:
for structure in self.nest.structures:
if structure.name in self._structure:
for device in structure.protectdevices:
for device in structure.smoke_co_alarms:
yield(structure, device)
else:
_LOGGER.info("Ignoring structure %s, not in %s",
@ -160,12 +160,12 @@ class NestDevice(object):
_LOGGER.error(
"Connection error logging into the nest web service.")
def camera_devices(self):
"""Generator returning list of camera devices."""
def cameras(self):
"""Generator returning list of cameras."""
try:
for structure in self.nest.structures:
if structure.name in self._structure:
for device in structure.cameradevices:
for device in structure.cameras:
yield(structure, device)
else:
_LOGGER.info("Ignoring structure %s, not in %s",
@ -173,18 +173,3 @@ class NestDevice(object):
except socket.error:
_LOGGER.error(
"Connection error logging into the nest web service.")
def is_thermostat(device):
"""Target devices that are Nest Thermostats."""
return bool(device.__class__.__name__ == 'Device')
def is_protect(device):
"""Target devices that are Nest Protect Smoke Alarms."""
return bool(device.__class__.__name__ == 'ProtectDevice')
def is_camera(device):
"""Target devices that are Nest Protect Smoke Alarms."""
return bool(device.__class__.__name__ == 'CameraDevice')

View File

@ -115,7 +115,7 @@ def async_setup(hass, config):
update_coro = hass.loop.create_task(
remote.async_update_ha_state(True))
if hasattr(remote, 'async_update'):
update_tasks.append(hass.loop.create_task(update_coro))
update_tasks.append(update_coro)
else:
yield from update_coro

View File

@ -9,7 +9,8 @@ import logging
import voluptuous as vol
from homeassistant.components.nest import DATA_NEST, DOMAIN
from homeassistant.components.nest import (
DATA_NEST, DOMAIN)
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_PLATFORM,
@ -93,31 +94,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error(wstr)
all_sensors = []
for structure, device in chain(nest.devices(), nest.protect_devices()):
for structure, device in chain(nest.thermostats(), nest.smoke_co_alarms()):
sensors = [NestBasicSensor(structure, device, variable)
for variable in conf
if variable in SENSOR_TYPES and is_thermostat(device)]
if variable in SENSOR_TYPES and device.is_thermostat]
sensors += [NestTempSensor(structure, device, variable)
for variable in conf
if variable in SENSOR_TEMP_TYPES and is_thermostat(device)]
if variable in SENSOR_TEMP_TYPES and device.is_thermostat]
sensors += [NestProtectSensor(structure, device, variable)
for variable in conf
if variable in PROTECT_VARS and is_protect(device)]
if variable in PROTECT_VARS and device.is_smoke_co_alarm]
all_sensors.extend(sensors)
add_devices(all_sensors, True)
def is_thermostat(device):
"""Target devices that are Nest Thermostats."""
return bool(device.__class__.__name__ == 'Device')
def is_protect(device):
"""Target devices that are Nest Protect Smoke Alarms."""
return bool(device.__class__.__name__ == 'ProtectDevice')
class NestSensor(Entity):
"""Representation of a Nest sensor."""

View File

@ -98,7 +98,7 @@ def async_setup(hass, config):
update_coro = hass.loop.create_task(
switch.async_update_ha_state(True))
if hasattr(switch, 'async_update'):
update_tasks.append(hass.loop.create_task(update_coro))
update_tasks.append(update_coro)
else:
yield from update_coro

View File

@ -3,12 +3,16 @@ say:
fields:
entity_id:
description: Name(s) of media player entities
description: Name(s) of media player entities.
example: 'media_player.floor'
message:
description: Text to speak on devices
description: Text to speak on devices.
example: 'My name is hanna'
cache:
description: Control file cache of this message.
example: 'true'
clear_cache:
description: Remove cache files and RAM cache.

View File

@ -21,6 +21,18 @@ _LOGGER = logging.getLogger(__name__)
VOICERSS_API_URL = "https://api.voicerss.org/"
ERROR_MSG = [
b'Error description',
b'The subscription is expired or requests count limitation is exceeded!',
b'The request content length is too large!',
b'The language does not support!',
b'The language is not specified!',
b'The text is not specified!',
b'The API key is not available!',
b'The API key is not specified!',
b'The subscription does not support SSML!',
]
SUPPORT_LANGUAGES = [
'ca-es', 'zh-cn', 'zh-hk', 'zh-tw', 'da-dk', 'nl-nl', 'en-au', 'en-ca',
'en-gb', 'en-in', 'en-us', 'fi-fi', 'fr-ca', 'fr-fr', 'de-de', 'it-it',
@ -83,7 +95,7 @@ class VoiceRSSProvider(Provider):
self.hass = hass
self.extension = conf.get(CONF_CODEC)
self.params = {
self.form_data = {
'key': conf.get(CONF_API_KEY),
'hl': conf.get(CONF_LANG),
'c': (conf.get(CONF_CODEC)).upper(),
@ -94,21 +106,28 @@ class VoiceRSSProvider(Provider):
def async_get_tts_audio(self, message):
"""Load TTS from voicerss."""
websession = async_get_clientsession(self.hass)
form_data = self.form_data.copy()
form_data['src'] = message
request = None
try:
with async_timeout.timeout(10, loop=self.hass.loop):
request = yield from websession.post(
VOICERSS_API_URL, params=self.params,
data=bytes(message, 'utf-8')
VOICERSS_API_URL, data=form_data
)
if request.status != 200:
_LOGGER.error("Error %d on load url %s",
_LOGGER.error("Error %d on load url %s.",
request.status, request.url)
return (None, None)
data = yield from request.read()
if data in ERROR_MSG:
_LOGGER.error(
"Error receive %s from voicerss.", str(data, 'utf-8'))
return (None, None)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.error("Timeout for voicerss api.")
return (None, None)

View File

@ -2,7 +2,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 35
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)

View File

@ -55,7 +55,9 @@ class AsyncHandler(object):
When blocking=True, will wait till closed.
"""
self.close()
if not self._thread.is_alive():
return
yield from self._queue.put(None)
if blocking:
# Python 3.4.4+

View File

@ -174,7 +174,7 @@ hikvision==0.4
# http://github.com/adafruit/Adafruit_Python_DHT/archive/310c59b0293354d07d94375f1365f7b9b9110c7d.zip#Adafruit_DHT==1.3.0
# homeassistant.components.nest
http://github.com/technicalpickles/python-nest/archive/b8391d2b3cb8682f8b0c2bdff477179983609f39.zip#python-nest==3.0.2
http://github.com/technicalpickles/python-nest/archive/e6c9d56a8df455d4d7746389811f2c1387e8cb33.zip#python-nest==3.0.3
# homeassistant.components.light.flux_led
https://github.com/Danielhiversen/flux_led/archive/0.10.zip#flux_led==0.10

View File

@ -20,11 +20,12 @@ class TestTTSVoiceRSSPlatform(object):
self.hass = get_test_home_assistant()
self.url = "https://api.voicerss.org/"
self.url_param = {
self.form_data = {
'key': '1234567xx',
'hl': 'en-us',
'c': 'MP3',
'f': '8khz_8bit_mono',
'src': "I person is on front of your door.",
}
def teardown_method(self):
@ -63,7 +64,7 @@ class TestTTSVoiceRSSPlatform(object):
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
aioclient_mock.post(
self.url, params=self.url_param, status=200, content=b'test')
self.url, data=self.form_data, status=200, content=b'test')
config = {
tts.DOMAIN: {
@ -82,15 +83,16 @@ class TestTTSVoiceRSSPlatform(object):
assert len(calls) == 1
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[0][2] == self.form_data
assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1
def test_service_say_german(self, aioclient_mock):
"""Test service call say with german code."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
self.url_param['hl'] = 'de-de'
self.form_data['hl'] = 'de-de'
aioclient_mock.post(
self.url, params=self.url_param, status=200, content=b'test')
self.url, data=self.form_data, status=200, content=b'test')
config = {
tts.DOMAIN: {
@ -110,13 +112,14 @@ class TestTTSVoiceRSSPlatform(object):
assert len(calls) == 1
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[0][2] == self.form_data
def test_service_say_error(self, aioclient_mock):
"""Test service call say with http response 400."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
aioclient_mock.post(
self.url, params=self.url_param, status=400, content=b'test')
self.url, data=self.form_data, status=400, content=b'test')
config = {
tts.DOMAIN: {
@ -135,13 +138,14 @@ class TestTTSVoiceRSSPlatform(object):
assert len(calls) == 0
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[0][2] == self.form_data
def test_service_say_timeout(self, aioclient_mock):
"""Test service call say with http timeout."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
aioclient_mock.post(
self.url, params=self.url_param, exc=asyncio.TimeoutError())
self.url, data=self.form_data, exc=asyncio.TimeoutError())
config = {
tts.DOMAIN: {
@ -160,3 +164,32 @@ class TestTTSVoiceRSSPlatform(object):
assert len(calls) == 0
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[0][2] == self.form_data
def test_service_say_error_msg(self, aioclient_mock):
"""Test service call say with http error api message."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
aioclient_mock.post(
self.url, data=self.form_data, status=200,
content=b'The subscription does not support SSML!'
)
config = {
tts.DOMAIN: {
'platform': 'voicerss',
'api_key': '1234567xx',
}
}
with assert_setup_component(1, tts.DOMAIN):
setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call(tts.DOMAIN, 'voicerss_say', {
tts.ATTR_MESSAGE: "I person is on front of your door.",
})
self.hass.block_till_done()
assert len(calls) == 0
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[0][2] == self.form_data