Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 308d71c448 | |||
| 203c1cfc96 | |||
| 9bf4a53fbb | |||
| f18a88c2d4 | |||
| 35b4da0aa2 | |||
| 61fc4ca8fe | |||
| 3ea984ca25 | |||
| 1258c4c680 | |||
| 75dd391118 | |||
| 76a9eba744 | |||
| 31fe1d28e8 | |||
| a4a38c8a00 | |||
| 3b74cc606e | |||
| b750319de4 |
@@ -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
|
||||
|
||||
|
||||
@@ -166,7 +166,9 @@ def async_setup(hass, config):
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(entity.async_trigger(
|
||||
service_call.data.get(ATTR_VARIABLES), True))
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def turn_onoff_service_handler(service_call):
|
||||
@@ -175,7 +177,9 @@ def async_setup(hass, config):
|
||||
method = 'async_{}'.format(service_call.service)
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(getattr(entity, method)())
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def toggle_service_handler(service_call):
|
||||
@@ -186,7 +190,9 @@ def async_setup(hass, config):
|
||||
tasks.append(entity.async_turn_off())
|
||||
else:
|
||||
tasks.append(entity.async_turn_on())
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.util import get_local_ip
|
||||
import homeassistant.util as hass_util
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS,
|
||||
@@ -41,6 +41,7 @@ REQUIREMENTS = ('aiohttp_cors==0.5.0',)
|
||||
CONF_API_PASSWORD = 'api_password'
|
||||
CONF_SERVER_HOST = 'server_host'
|
||||
CONF_SERVER_PORT = 'server_port'
|
||||
CONF_BASE_URL = 'base_url'
|
||||
CONF_DEVELOPMENT = 'development'
|
||||
CONF_SSL_CERTIFICATE = 'ssl_certificate'
|
||||
CONF_SSL_KEY = 'ssl_key'
|
||||
@@ -84,6 +85,7 @@ HTTP_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string,
|
||||
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_BASE_URL): cv.string,
|
||||
vol.Optional(CONF_DEVELOPMENT, default=DEFAULT_DEVELOPMENT): cv.string,
|
||||
vol.Optional(CONF_SSL_CERTIFICATE, default=None): cv.isfile,
|
||||
vol.Optional(CONF_SSL_KEY, default=None): cv.isfile,
|
||||
@@ -155,9 +157,19 @@ def async_setup(hass, config):
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server)
|
||||
|
||||
hass.http = server
|
||||
hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
|
||||
else get_local_ip(),
|
||||
api_password, server_port,
|
||||
|
||||
host = conf.get(CONF_BASE_URL)
|
||||
|
||||
if host:
|
||||
port = None
|
||||
elif server_host != DEFAULT_SERVER_HOST:
|
||||
host = server_host
|
||||
port = server_port
|
||||
else:
|
||||
host = hass_util.get_local_ip()
|
||||
port = server_port
|
||||
|
||||
hass.config.api = rem.API(host, api_password, port,
|
||||
ssl_certificate is not None)
|
||||
|
||||
return True
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -157,7 +157,8 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_CLEAR_CACHE, async_clear_cache_handle,
|
||||
descriptions.get(SERVICE_CLEAR_CACHE), schema=SERVICE_CLEAR_CACHE)
|
||||
descriptions.get(SERVICE_CLEAR_CACHE),
|
||||
schema=SCHEMA_SERVICE_CLEAR_CACHE)
|
||||
|
||||
return True
|
||||
|
||||
@@ -170,9 +171,9 @@ class SpeechManager(object):
|
||||
self.hass = hass
|
||||
self.providers = {}
|
||||
|
||||
self.use_cache = True
|
||||
self.cache_dir = None
|
||||
self.time_memory = None
|
||||
self.use_cache = DEFAULT_CACHE
|
||||
self.cache_dir = DEFAULT_CACHE_DIR
|
||||
self.time_memory = DEFAULT_TIME_MEMORY
|
||||
self.file_cache = {}
|
||||
self.mem_cache = {}
|
||||
|
||||
@@ -229,7 +230,7 @@ class SpeechManager(object):
|
||||
"""Remove files from filesystem."""
|
||||
for _, filename in self.file_cache.items():
|
||||
try:
|
||||
os.remove(os.path.join(self.cache_dir), filename)
|
||||
os.remove(os.path.join(self.cache_dir, filename))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -54,6 +54,8 @@ frontend:
|
||||
http:
|
||||
# Uncomment this to add a password (recommended!)
|
||||
# api_password: PASSWORD
|
||||
# Uncomment this if you are using SSL or running in Docker etc
|
||||
# base_url: example.duckdns.org:8123
|
||||
|
||||
# Checks for available updates
|
||||
updater:
|
||||
@@ -76,6 +78,11 @@ sun:
|
||||
# Weather Prediction
|
||||
sensor:
|
||||
platform: yr
|
||||
|
||||
# Text to speech
|
||||
tts:
|
||||
platform: google
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 35
|
||||
PATCH_VERSION = '0'
|
||||
PATCH_VERSION = '3'
|
||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||
REQUIRED_PYTHON_VER = (3, 4, 2)
|
||||
|
||||
@@ -298,9 +298,8 @@ class HomeAssistant(object):
|
||||
# cleanup async layer from python logging
|
||||
if self.data.get(DATA_ASYNCHANDLER):
|
||||
handler = self.data.pop(DATA_ASYNCHANDLER)
|
||||
logger = logging.getLogger('')
|
||||
handler.close()
|
||||
logger.removeHandler(handler)
|
||||
logging.getLogger('').removeHandler(handler)
|
||||
yield from handler.async_close(blocking=True)
|
||||
|
||||
self.loop.stop()
|
||||
|
||||
|
||||
+11
-6
@@ -55,15 +55,20 @@ class API(object):
|
||||
"""Object to pass around Home Assistant API location and credentials."""
|
||||
|
||||
def __init__(self, host: str, api_password: Optional[str]=None,
|
||||
port: Optional[int]=None, use_ssl: bool=False) -> None:
|
||||
port: Optional[int]=SERVER_PORT, use_ssl: bool=False) -> None:
|
||||
"""Initalize the API."""
|
||||
self.host = host
|
||||
self.port = port or SERVER_PORT
|
||||
self.port = port
|
||||
self.api_password = api_password
|
||||
|
||||
if use_ssl:
|
||||
self.base_url = "https://{}:{}".format(host, self.port)
|
||||
self.base_url = "https://{}".format(host)
|
||||
else:
|
||||
self.base_url = "http://{}:{}".format(host, self.port)
|
||||
self.base_url = "http://{}".format(host)
|
||||
|
||||
if port is not None:
|
||||
self.base_url += ':{}'.format(port)
|
||||
|
||||
self.status = None
|
||||
self._headers = {
|
||||
HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON,
|
||||
@@ -106,8 +111,8 @@ class API(object):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return the representation of the API."""
|
||||
return "API({}, {}, {})".format(
|
||||
self.host, self.api_password, self.port)
|
||||
return "<API({}, password: {})>".format(
|
||||
self.base_url, 'yes' if self.api_password is not None else 'no')
|
||||
|
||||
|
||||
class HomeAssistant(ha.HomeAssistant):
|
||||
|
||||
@@ -49,6 +49,25 @@ class AsyncHandler(object):
|
||||
"""Wrap close to handler."""
|
||||
self.emit(None)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close(self, blocking=False):
|
||||
"""Close the handler.
|
||||
|
||||
When blocking=True, will wait till closed.
|
||||
"""
|
||||
if not self._thread.is_alive():
|
||||
return
|
||||
yield from self._queue.put(None)
|
||||
|
||||
if blocking:
|
||||
# Python 3.4.4+
|
||||
# pylint: disable=no-member
|
||||
if hasattr(self._queue, 'join'):
|
||||
yield from self._queue.join()
|
||||
else:
|
||||
while not self._queue.empty():
|
||||
yield from asyncio.sleep(0, loop=self.loop)
|
||||
|
||||
def emit(self, record):
|
||||
"""Process a record."""
|
||||
ident = self.loop.__dict__.get("_thread_ident")
|
||||
@@ -66,15 +85,23 @@ class AsyncHandler(object):
|
||||
|
||||
def _process(self):
|
||||
"""Process log in a thread."""
|
||||
support_join = hasattr(self._queue, 'task_done')
|
||||
|
||||
while True:
|
||||
record = run_coroutine_threadsafe(
|
||||
self._queue.get(), self.loop).result()
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
||||
if record is None:
|
||||
self.handler.close()
|
||||
if support_join:
|
||||
self.loop.call_soon_threadsafe(self._queue.task_done)
|
||||
return
|
||||
|
||||
self.handler.emit(record)
|
||||
if support_join:
|
||||
self.loop.call_soon_threadsafe(self._queue.task_done)
|
||||
|
||||
def createLock(self):
|
||||
"""Ignore lock stuff."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""The tests for the Home Assistant HTTP component."""
|
||||
import asyncio
|
||||
import requests
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeassistant import bootstrap, const
|
||||
import homeassistant.components.http as http
|
||||
@@ -154,3 +155,49 @@ def test_registering_view_while_running(hass, test_client):
|
||||
|
||||
text = yield from resp.text()
|
||||
assert text == 'hello'
|
||||
|
||||
|
||||
def test_api_base_url(loop):
|
||||
"""Test setting api url."""
|
||||
|
||||
hass = MagicMock()
|
||||
hass.loop = loop
|
||||
|
||||
assert loop.run_until_complete(
|
||||
bootstrap.async_setup_component(hass, 'http', {
|
||||
'http': {
|
||||
'base_url': 'example.com'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
assert hass.config.api.base_url == 'http://example.com'
|
||||
|
||||
assert loop.run_until_complete(
|
||||
bootstrap.async_setup_component(hass, 'http', {
|
||||
'http': {
|
||||
'server_host': '1.1.1.1'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
assert hass.config.api.base_url == 'http://1.1.1.1:8123'
|
||||
|
||||
assert loop.run_until_complete(
|
||||
bootstrap.async_setup_component(hass, 'http', {
|
||||
'http': {
|
||||
'server_host': '1.1.1.1'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
assert hass.config.api.base_url == 'http://1.1.1.1:8123'
|
||||
|
||||
assert loop.run_until_complete(
|
||||
bootstrap.async_setup_component(hass, 'http', {
|
||||
'http': {
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
assert hass.config.api.base_url == 'http://127.0.0.1:8123'
|
||||
|
||||
@@ -88,35 +88,35 @@ class TestTTS(object):
|
||||
self.default_tts_cache,
|
||||
"265944c108cbb00b2a621be5930513e03a0bb2cd_demo.mp3"))
|
||||
|
||||
def test_setup_component_and_test_service_clear_cache(self):
|
||||
"""Setup the demo platform and call service clear cache."""
|
||||
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
|
||||
def test_setup_component_and_test_service_clear_cache(self):
|
||||
"""Setup the demo platform and call service clear cache."""
|
||||
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
|
||||
|
||||
config = {
|
||||
tts.DOMAIN: {
|
||||
'platform': 'demo',
|
||||
}
|
||||
config = {
|
||||
tts.DOMAIN: {
|
||||
'platform': 'demo',
|
||||
}
|
||||
}
|
||||
|
||||
with assert_setup_component(1, tts.DOMAIN):
|
||||
setup_component(self.hass, tts.DOMAIN, config)
|
||||
with assert_setup_component(1, tts.DOMAIN):
|
||||
setup_component(self.hass, tts.DOMAIN, config)
|
||||
|
||||
self.hass.services.call(tts.DOMAIN, 'demo_say', {
|
||||
tts.ATTR_MESSAGE: "I person is on front of your door.",
|
||||
})
|
||||
self.hass.block_till_done()
|
||||
self.hass.services.call(tts.DOMAIN, 'demo_say', {
|
||||
tts.ATTR_MESSAGE: "I person is on front of your door.",
|
||||
})
|
||||
self.hass.block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
assert os.path.isfile(os.path.join(
|
||||
self.default_tts_cache,
|
||||
"265944c108cbb00b2a621be5930513e03a0bb2cd_demo.mp3"))
|
||||
assert len(calls) == 1
|
||||
assert os.path.isfile(os.path.join(
|
||||
self.default_tts_cache,
|
||||
"265944c108cbb00b2a621be5930513e03a0bb2cd_demo.mp3"))
|
||||
|
||||
self.hass.services.call(tts.DOMAIN, tts.SERVICE_CLEAR_CACHE, {})
|
||||
self.hass.block_till_done()
|
||||
self.hass.services.call(tts.DOMAIN, tts.SERVICE_CLEAR_CACHE, {})
|
||||
self.hass.block_till_done()
|
||||
|
||||
assert not os.path.isfile(os.path.join(
|
||||
self.default_tts_cache,
|
||||
"265944c108cbb00b2a621be5930513e03a0bb2cd_demo.mp3"))
|
||||
assert not os.path.isfile(os.path.join(
|
||||
self.default_tts_cache,
|
||||
"265944c108cbb00b2a621be5930513e03a0bb2cd_demo.mp3"))
|
||||
|
||||
def test_setup_component_and_test_service_with_receive_voice(self):
|
||||
"""Setup the demo platform and call service and receive voice."""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user