Merge pull request #15149 from home-assistant/rc

0.72.1
pull/15307/head 0.72.1
Paulus Schoutsen 2018-06-25 17:25:44 -04:00 committed by GitHub
commit d58e401812
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 143 additions and 65 deletions

View File

@ -53,7 +53,6 @@ class YiCamera(Camera):
"""Initialize."""
super().__init__()
self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
self._ftp = None
self._last_image = None
self._last_url = None
self._manager = hass.data[DATA_FFMPEG]
@ -64,8 +63,6 @@ class YiCamera(Camera):
self.user = config[CONF_USERNAME]
self.passwd = config[CONF_PASSWORD]
hass.async_add_job(self._connect_to_client)
@property
def brand(self):
"""Camera brand."""
@ -76,38 +73,35 @@ class YiCamera(Camera):
"""Return the name of this camera."""
return self._name
async def _connect_to_client(self):
"""Attempt to establish a connection via FTP."""
async def _get_latest_video_url(self):
"""Retrieve the latest video file from the customized Yi FTP server."""
from aioftp import Client, StatusCodeError
ftp = Client()
try:
await ftp.connect(self.host)
await ftp.login(self.user, self.passwd)
self._ftp = ftp
except StatusCodeError as err:
raise PlatformNotReady(err)
async def _get_latest_video_url(self):
"""Retrieve the latest video file from the customized Yi FTP server."""
from aioftp import StatusCodeError
try:
await self._ftp.change_directory(self.path)
await ftp.change_directory(self.path)
dirs = []
for path, attrs in await self._ftp.list():
for path, attrs in await ftp.list():
if attrs['type'] == 'dir' and '.' not in str(path):
dirs.append(path)
latest_dir = dirs[-1]
await self._ftp.change_directory(latest_dir)
await ftp.change_directory(latest_dir)
videos = []
for path, _ in await self._ftp.list():
for path, _ in await ftp.list():
videos.append(path)
if not videos:
_LOGGER.info('Video folder "%s" empty; delaying', latest_dir)
return None
await ftp.quit()
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
self.user, self.passwd, self.host, self.port, self.path,
latest_dir, videos[-1])

View File

@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
from homeassistant.util.yaml import load_yaml
REQUIREMENTS = ['home-assistant-frontend==20180622.1']
REQUIREMENTS = ['home-assistant-frontend==20180625.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']

View File

@ -4,7 +4,7 @@ Provide functionality to interact with Cast devices on the network.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.cast/
"""
# pylint: disable=import-error
import asyncio
import logging
import threading
from typing import Optional, Tuple
@ -200,9 +200,13 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up Cast from a config entry."""
await _async_setup_platform(
hass, hass.data[CAST_DOMAIN].get('media_player', {}),
async_add_devices, None)
config = hass.data[CAST_DOMAIN].get('media_player', {})
if not isinstance(config, list):
config = [config]
await asyncio.wait([
_async_setup_platform(hass, cfg, async_add_devices, None)
for cfg in config])
async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType,

View File

@ -23,7 +23,7 @@ from homeassistant.helpers.entity import Entity
from .const import DOMAIN
from . import local_auth
REQUIREMENTS = ['python-nest==4.0.2']
REQUIREMENTS = ['python-nest==4.0.3']
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
@ -86,6 +86,7 @@ async def async_nest_update_event_broker(hass, nest):
_LOGGER.debug("dispatching nest data update")
async_dispatcher_send(hass, SIGNAL_NEST_UPDATE)
else:
_LOGGER.debug("stop listening nest.update_event")
return
@ -122,7 +123,8 @@ async def async_setup_entry(hass, entry):
_LOGGER.debug("proceeding with setup")
conf = hass.data.get(DATA_NEST_CONFIG, {})
hass.data[DATA_NEST] = NestDevice(hass, conf, nest)
await hass.async_add_job(hass.data[DATA_NEST].initialize)
if not await hass.async_add_job(hass.data[DATA_NEST].initialize):
return False
for component in 'climate', 'camera', 'sensor', 'binary_sensor':
hass.async_add_job(hass.config_entries.async_forward_entry_setup(
@ -192,63 +194,73 @@ class NestDevice(object):
def initialize(self):
"""Initialize Nest."""
if self.local_structure is None:
self.local_structure = [s.name for s in self.nest.structures]
from nest.nest import AuthorizationError, APIError
try:
# Do not optimize next statement, it is here for initialize
# persistence Nest API connection.
structure_names = [s.name for s in self.nest.structures]
if self.local_structure is None:
self.local_structure = structure_names
except (AuthorizationError, APIError, socket.error) as err:
_LOGGER.error(
"Connection error while access Nest web service: %s", err)
return False
return True
def structures(self):
"""Generate a list of structures."""
from nest.nest import AuthorizationError, APIError
try:
for structure in self.nest.structures:
if structure.name in self.local_structure:
yield structure
else:
if structure.name not in self.local_structure:
_LOGGER.debug("Ignoring structure %s, not in %s",
structure.name, self.local_structure)
except socket.error:
continue
yield structure
except (AuthorizationError, APIError, socket.error) as err:
_LOGGER.error(
"Connection error logging into the nest web service.")
"Connection error while access Nest web service: %s", err)
def thermostats(self):
"""Generate a list of thermostats and their location."""
try:
for structure in self.nest.structures:
if structure.name in self.local_structure:
for device in structure.thermostats:
yield (structure, device)
else:
_LOGGER.debug("Ignoring structure %s, not in %s",
structure.name, self.local_structure)
except socket.error:
_LOGGER.error(
"Connection error logging into the nest web service.")
"""Generate a list of thermostats."""
return self._devices('thermostats')
def smoke_co_alarms(self):
"""Generate a list of smoke co alarms."""
try:
for structure in self.nest.structures:
if structure.name in self.local_structure:
for device in structure.smoke_co_alarms:
yield (structure, device)
else:
_LOGGER.debug("Ignoring structure %s, not in %s",
structure.name, self.local_structure)
except socket.error:
_LOGGER.error(
"Connection error logging into the nest web service.")
return self._devices('smoke_co_alarms')
def cameras(self):
"""Generate a list of cameras."""
return self._devices('cameras')
def _devices(self, device_type):
"""Generate a list of Nest devices."""
from nest.nest import AuthorizationError, APIError
try:
for structure in self.nest.structures:
if structure.name in self.local_structure:
for device in structure.cameras:
yield (structure, device)
else:
if structure.name not in self.local_structure:
_LOGGER.debug("Ignoring structure %s, not in %s",
structure.name, self.local_structure)
except socket.error:
continue
for device in getattr(structure, device_type, []):
try:
# Do not optimize next statement,
# it is here for verify Nest API permission.
device.name_long
except KeyError:
_LOGGER.warning("Cannot retrieve device name for [%s]"
", please check your Nest developer "
"account permission settings.",
device.serial)
continue
yield (structure, device)
except (AuthorizationError, APIError, socket.error) as err:
_LOGGER.error(
"Connection error logging into the nest web service.")
"Connection error while access Nest web service: %s", err)
class NestSensorDevice(Entity):

View File

@ -24,10 +24,14 @@ PROTECT_SENSOR_TYPES = ['co_status',
# color_status: "gray", "green", "yellow", "red"
'color_status']
STRUCTURE_SENSOR_TYPES = ['eta', 'security_state']
STRUCTURE_SENSOR_TYPES = ['eta']
# security_state is structure level sensor, but only meaningful when
# Nest Cam exist
STRUCTURE_CAMERA_SENSOR_TYPES = ['security_state']
_VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \
+ STRUCTURE_SENSOR_TYPES
+ STRUCTURE_SENSOR_TYPES + STRUCTURE_CAMERA_SENSOR_TYPES
SENSOR_UNITS = {'humidity': '%'}
@ -105,6 +109,14 @@ async def async_setup_entry(hass, entry, async_add_devices):
for variable in conditions
if variable in PROTECT_SENSOR_TYPES]
structures_has_camera = {}
for structure, device in nest.cameras():
structures_has_camera[structure] = True
for structure in structures_has_camera:
all_sensors += [NestBasicSensor(structure, None, variable)
for variable in conditions
if variable in STRUCTURE_CAMERA_SENSOR_TYPES]
return all_sensors
async_add_devices(await hass.async_add_job(get_sensors), True)
@ -133,7 +145,8 @@ class NestBasicSensor(NestSensorDevice):
elif self.variable in PROTECT_SENSOR_TYPES \
and self.variable != 'color_status':
# keep backward compatibility
self._state = getattr(self.device, self.variable).capitalize()
state = getattr(self.device, self.variable)
self._state = state.capitalize() if state is not None else None
else:
self._state = getattr(self.device, self.variable)

View File

@ -2,7 +2,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 72
PATCH_VERSION = '0'
PATCH_VERSION = '1'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)

View File

@ -404,7 +404,7 @@ hipnotify==1.0.8
holidays==0.9.5
# homeassistant.components.frontend
home-assistant-frontend==20180622.1
home-assistant-frontend==20180625.0
# homeassistant.components.homekit_controller
# homekit==0.6
@ -1060,7 +1060,7 @@ python-mpd2==1.0.0
python-mystrom==0.4.4
# homeassistant.components.nest
python-nest==4.0.2
python-nest==4.0.3
# homeassistant.components.device_tracker.nmap_tracker
python-nmap==0.6.1

View File

@ -81,7 +81,7 @@ hbmqtt==0.9.2
holidays==0.9.5
# homeassistant.components.frontend
home-assistant-frontend==20180622.1
home-assistant-frontend==20180625.0
# homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb
@ -156,7 +156,7 @@ pyqwikswitch==0.8
python-forecastio==1.4.0
# homeassistant.components.nest
python-nest==4.0.2
python-nest==4.0.3
# homeassistant.components.sensor.whois
pythonwhois==2.4.3

View File

@ -17,6 +17,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect, \
from homeassistant.components.media_player import cast
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, mock_coro
@pytest.fixture(autouse=True)
def cast_mock():
@ -359,3 +361,56 @@ async def test_disconnect_on_stop(hass: HomeAssistantType):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
assert chromecast.disconnect.call_count == 1
async def test_entry_setup_no_config(hass: HomeAssistantType):
"""Test setting up entry with no config.."""
await async_setup_component(hass, 'cast', {})
with patch(
'homeassistant.components.media_player.cast._async_setup_platform',
return_value=mock_coro()) as mock_setup:
await cast.async_setup_entry(hass, MockConfigEntry(), None)
assert len(mock_setup.mock_calls) == 1
assert mock_setup.mock_calls[0][1][1] == {}
async def test_entry_setup_single_config(hass: HomeAssistantType):
"""Test setting up entry and having a single config option."""
await async_setup_component(hass, 'cast', {
'cast': {
'media_player': {
'host': 'bla'
}
}
})
with patch(
'homeassistant.components.media_player.cast._async_setup_platform',
return_value=mock_coro()) as mock_setup:
await cast.async_setup_entry(hass, MockConfigEntry(), None)
assert len(mock_setup.mock_calls) == 1
assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'}
async def test_entry_setup_list_config(hass: HomeAssistantType):
"""Test setting up entry and having multiple config options."""
await async_setup_component(hass, 'cast', {
'cast': {
'media_player': [
{'host': 'bla'},
{'host': 'blu'},
]
}
})
with patch(
'homeassistant.components.media_player.cast._async_setup_platform',
return_value=mock_coro()) as mock_setup:
await cast.async_setup_entry(hass, MockConfigEntry(), None)
assert len(mock_setup.mock_calls) == 2
assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'}
assert mock_setup.mock_calls[1][1][1] == {'host': 'blu'}