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( update_coro = hass.loop.create_task(
alarm.async_update_ha_state(True)) alarm.async_update_ha_state(True))
if hasattr(alarm, 'async_update'): if hasattr(alarm, 'async_update'):
update_tasks.append(hass.loop.create_task(update_coro)) update_tasks.append(update_coro)
else: else:
yield from update_coro yield from update_coro

View File

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

View File

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

View File

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

View File

@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = [ REQUIREMENTS = [
'http://github.com/technicalpickles/python-nest' 'http://github.com/technicalpickles/python-nest'
'/archive/b8391d2b3cb8682f8b0c2bdff477179983609f39.zip' # nest-cam branch '/archive/e6c9d56a8df455d4d7746389811f2c1387e8cb33.zip' # nest-cam branch
'#python-nest==3.0.2'] '#python-nest==3.0.3']
DOMAIN = 'nest' DOMAIN = 'nest'
@ -132,12 +132,12 @@ class NestDevice(object):
self._structure = conf[CONF_STRUCTURE] self._structure = conf[CONF_STRUCTURE]
_LOGGER.debug("Structures to include: %s", self._structure) _LOGGER.debug("Structures to include: %s", self._structure)
def devices(self): def thermostats(self):
"""Generator returning list of devices and their location.""" """Generator returning list of thermostats and their location."""
try: try:
for structure in self.nest.structures: for structure in self.nest.structures:
if structure.name in self._structure: if structure.name in self._structure:
for device in structure.devices: for device in structure.thermostats:
yield (structure, device) yield (structure, device)
else: else:
_LOGGER.debug("Ignoring structure %s, not in %s", _LOGGER.debug("Ignoring structure %s, not in %s",
@ -146,12 +146,12 @@ class NestDevice(object):
_LOGGER.error( _LOGGER.error(
"Connection error logging into the nest web service.") "Connection error logging into the nest web service.")
def protect_devices(self): def smoke_co_alarms(self):
"""Generator returning list of protect devices.""" """Generator returning list of smoke co alarams."""
try: try:
for structure in self.nest.structures: for structure in self.nest.structures:
if structure.name in self._structure: if structure.name in self._structure:
for device in structure.protectdevices: for device in structure.smoke_co_alarms:
yield(structure, device) yield(structure, device)
else: else:
_LOGGER.info("Ignoring structure %s, not in %s", _LOGGER.info("Ignoring structure %s, not in %s",
@ -160,12 +160,12 @@ class NestDevice(object):
_LOGGER.error( _LOGGER.error(
"Connection error logging into the nest web service.") "Connection error logging into the nest web service.")
def camera_devices(self): def cameras(self):
"""Generator returning list of camera devices.""" """Generator returning list of cameras."""
try: try:
for structure in self.nest.structures: for structure in self.nest.structures:
if structure.name in self._structure: if structure.name in self._structure:
for device in structure.cameradevices: for device in structure.cameras:
yield(structure, device) yield(structure, device)
else: else:
_LOGGER.info("Ignoring structure %s, not in %s", _LOGGER.info("Ignoring structure %s, not in %s",
@ -173,18 +173,3 @@ class NestDevice(object):
except socket.error: except socket.error:
_LOGGER.error( _LOGGER.error(
"Connection error logging into the nest web service.") "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( update_coro = hass.loop.create_task(
remote.async_update_ha_state(True)) remote.async_update_ha_state(True))
if hasattr(remote, 'async_update'): if hasattr(remote, 'async_update'):
update_tasks.append(hass.loop.create_task(update_coro)) update_tasks.append(update_coro)
else: else:
yield from update_coro yield from update_coro

View File

@ -9,7 +9,8 @@ import logging
import voluptuous as vol 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.helpers.entity import Entity
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_PLATFORM, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_PLATFORM,
@ -93,31 +94,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error(wstr) _LOGGER.error(wstr)
all_sensors = [] 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) sensors = [NestBasicSensor(structure, device, variable)
for variable in conf 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) sensors += [NestTempSensor(structure, device, variable)
for variable in conf 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) sensors += [NestProtectSensor(structure, device, variable)
for variable in conf 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) all_sensors.extend(sensors)
add_devices(all_sensors, True) 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): class NestSensor(Entity):
"""Representation of a Nest sensor.""" """Representation of a Nest sensor."""

View File

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

View File

@ -3,12 +3,16 @@ say:
fields: fields:
entity_id: entity_id:
description: Name(s) of media player entities description: Name(s) of media player entities.
example: 'media_player.floor' example: 'media_player.floor'
message: message:
description: Text to speak on devices description: Text to speak on devices.
example: 'My name is hanna' example: 'My name is hanna'
cache:
description: Control file cache of this message.
example: 'true'
clear_cache: clear_cache:
description: Remove cache files and RAM 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/" 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 = [ SUPPORT_LANGUAGES = [
'ca-es', 'zh-cn', 'zh-hk', 'zh-tw', 'da-dk', 'nl-nl', 'en-au', 'en-ca', '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', '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.hass = hass
self.extension = conf.get(CONF_CODEC) self.extension = conf.get(CONF_CODEC)
self.params = { self.form_data = {
'key': conf.get(CONF_API_KEY), 'key': conf.get(CONF_API_KEY),
'hl': conf.get(CONF_LANG), 'hl': conf.get(CONF_LANG),
'c': (conf.get(CONF_CODEC)).upper(), 'c': (conf.get(CONF_CODEC)).upper(),
@ -94,21 +106,28 @@ class VoiceRSSProvider(Provider):
def async_get_tts_audio(self, message): def async_get_tts_audio(self, message):
"""Load TTS from voicerss.""" """Load TTS from voicerss."""
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
form_data = self.form_data.copy()
form_data['src'] = message
request = None request = None
try: try:
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
request = yield from websession.post( request = yield from websession.post(
VOICERSS_API_URL, params=self.params, VOICERSS_API_URL, data=form_data
data=bytes(message, 'utf-8')
) )
if request.status != 200: if request.status != 200:
_LOGGER.error("Error %d on load url %s", _LOGGER.error("Error %d on load url %s.",
request.status, request.url) request.status, request.url)
return (None, None) return (None, None)
data = yield from request.read() 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): except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.error("Timeout for voicerss api.") _LOGGER.error("Timeout for voicerss api.")
return (None, None) return (None, None)

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 = 35 MINOR_VERSION = 35
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

@ -55,7 +55,9 @@ class AsyncHandler(object):
When blocking=True, will wait till closed. When blocking=True, will wait till closed.
""" """
self.close() if not self._thread.is_alive():
return
yield from self._queue.put(None)
if blocking: if blocking:
# Python 3.4.4+ # 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 # http://github.com/adafruit/Adafruit_Python_DHT/archive/310c59b0293354d07d94375f1365f7b9b9110c7d.zip#Adafruit_DHT==1.3.0
# homeassistant.components.nest # 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 # homeassistant.components.light.flux_led
https://github.com/Danielhiversen/flux_led/archive/0.10.zip#flux_led==0.10 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.hass = get_test_home_assistant()
self.url = "https://api.voicerss.org/" self.url = "https://api.voicerss.org/"
self.url_param = { self.form_data = {
'key': '1234567xx', 'key': '1234567xx',
'hl': 'en-us', 'hl': 'en-us',
'c': 'MP3', 'c': 'MP3',
'f': '8khz_8bit_mono', 'f': '8khz_8bit_mono',
'src': "I person is on front of your door.",
} }
def teardown_method(self): def teardown_method(self):
@ -63,7 +64,7 @@ class TestTTSVoiceRSSPlatform(object):
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
aioclient_mock.post( 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 = { config = {
tts.DOMAIN: { tts.DOMAIN: {
@ -82,15 +83,16 @@ class TestTTSVoiceRSSPlatform(object):
assert len(calls) == 1 assert len(calls) == 1
assert len(aioclient_mock.mock_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 assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".mp3") != -1
def test_service_say_german(self, aioclient_mock): def test_service_say_german(self, aioclient_mock):
"""Test service call say with german code.""" """Test service call say with german code."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) 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( 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 = { config = {
tts.DOMAIN: { tts.DOMAIN: {
@ -110,13 +112,14 @@ class TestTTSVoiceRSSPlatform(object):
assert len(calls) == 1 assert len(calls) == 1
assert len(aioclient_mock.mock_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): def test_service_say_error(self, aioclient_mock):
"""Test service call say with http response 400.""" """Test service call say with http response 400."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
aioclient_mock.post( 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 = { config = {
tts.DOMAIN: { tts.DOMAIN: {
@ -135,13 +138,14 @@ class TestTTSVoiceRSSPlatform(object):
assert len(calls) == 0 assert len(calls) == 0
assert len(aioclient_mock.mock_calls) == 1 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): def test_service_say_timeout(self, aioclient_mock):
"""Test service call say with http timeout.""" """Test service call say with http timeout."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
aioclient_mock.post( aioclient_mock.post(
self.url, params=self.url_param, exc=asyncio.TimeoutError()) self.url, data=self.form_data, exc=asyncio.TimeoutError())
config = { config = {
tts.DOMAIN: { tts.DOMAIN: {
@ -160,3 +164,32 @@ class TestTTSVoiceRSSPlatform(object):
assert len(calls) == 0 assert len(calls) == 0
assert len(aioclient_mock.mock_calls) == 1 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