Merge pull request #15149 from home-assistant/rc

0.72.1
This commit is contained in:
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.""" """Initialize."""
super().__init__() super().__init__()
self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS) self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
self._ftp = None
self._last_image = None self._last_image = None
self._last_url = None self._last_url = None
self._manager = hass.data[DATA_FFMPEG] self._manager = hass.data[DATA_FFMPEG]
@ -64,8 +63,6 @@ class YiCamera(Camera):
self.user = config[CONF_USERNAME] self.user = config[CONF_USERNAME]
self.passwd = config[CONF_PASSWORD] self.passwd = config[CONF_PASSWORD]
hass.async_add_job(self._connect_to_client)
@property @property
def brand(self): def brand(self):
"""Camera brand.""" """Camera brand."""
@ -76,38 +73,35 @@ class YiCamera(Camera):
"""Return the name of this camera.""" """Return the name of this camera."""
return self._name return self._name
async def _connect_to_client(self): async def _get_latest_video_url(self):
"""Attempt to establish a connection via FTP.""" """Retrieve the latest video file from the customized Yi FTP server."""
from aioftp import Client, StatusCodeError from aioftp import Client, StatusCodeError
ftp = Client() ftp = Client()
try: try:
await ftp.connect(self.host) await ftp.connect(self.host)
await ftp.login(self.user, self.passwd) await ftp.login(self.user, self.passwd)
self._ftp = ftp
except StatusCodeError as err: except StatusCodeError as err:
raise PlatformNotReady(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: try:
await self._ftp.change_directory(self.path) await ftp.change_directory(self.path)
dirs = [] 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): if attrs['type'] == 'dir' and '.' not in str(path):
dirs.append(path) dirs.append(path)
latest_dir = dirs[-1] latest_dir = dirs[-1]
await self._ftp.change_directory(latest_dir) await ftp.change_directory(latest_dir)
videos = [] videos = []
for path, _ in await self._ftp.list(): for path, _ in await ftp.list():
videos.append(path) videos.append(path)
if not videos: if not videos:
_LOGGER.info('Video folder "%s" empty; delaying', latest_dir) _LOGGER.info('Video folder "%s" empty; delaying', latest_dir)
return None return None
await ftp.quit()
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format( return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
self.user, self.passwd, self.host, self.port, self.path, self.user, self.passwd, self.host, self.port, self.path,
latest_dir, videos[-1]) 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.loader import bind_hass
from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml import load_yaml
REQUIREMENTS = ['home-assistant-frontend==20180622.1'] REQUIREMENTS = ['home-assistant-frontend==20180625.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.cast/ https://home-assistant.io/components/media_player.cast/
""" """
# pylint: disable=import-error import asyncio
import logging import logging
import threading import threading
from typing import Optional, Tuple 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): async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up Cast from a config entry.""" """Set up Cast from a config entry."""
await _async_setup_platform( config = hass.data[CAST_DOMAIN].get('media_player', {})
hass, hass.data[CAST_DOMAIN].get('media_player', {}), if not isinstance(config, list):
async_add_devices, None) 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, 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 .const import DOMAIN
from . import local_auth from . import local_auth
REQUIREMENTS = ['python-nest==4.0.2'] REQUIREMENTS = ['python-nest==4.0.3']
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -86,6 +86,7 @@ async def async_nest_update_event_broker(hass, nest):
_LOGGER.debug("dispatching nest data update") _LOGGER.debug("dispatching nest data update")
async_dispatcher_send(hass, SIGNAL_NEST_UPDATE) async_dispatcher_send(hass, SIGNAL_NEST_UPDATE)
else: else:
_LOGGER.debug("stop listening nest.update_event")
return return
@ -122,7 +123,8 @@ async def async_setup_entry(hass, entry):
_LOGGER.debug("proceeding with setup") _LOGGER.debug("proceeding with setup")
conf = hass.data.get(DATA_NEST_CONFIG, {}) conf = hass.data.get(DATA_NEST_CONFIG, {})
hass.data[DATA_NEST] = NestDevice(hass, conf, nest) 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': for component in 'climate', 'camera', 'sensor', 'binary_sensor':
hass.async_add_job(hass.config_entries.async_forward_entry_setup( hass.async_add_job(hass.config_entries.async_forward_entry_setup(
@ -192,63 +194,73 @@ class NestDevice(object):
def initialize(self): def initialize(self):
"""Initialize Nest.""" """Initialize Nest."""
if self.local_structure is None: from nest.nest import AuthorizationError, APIError
self.local_structure = [s.name for s in self.nest.structures] 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): def structures(self):
"""Generate a list of structures.""" """Generate a list of structures."""
from nest.nest import AuthorizationError, APIError
try: try:
for structure in self.nest.structures: for structure in self.nest.structures:
if structure.name in self.local_structure: if structure.name not in self.local_structure:
yield structure
else:
_LOGGER.debug("Ignoring structure %s, not in %s", _LOGGER.debug("Ignoring structure %s, not in %s",
structure.name, self.local_structure) structure.name, self.local_structure)
except socket.error: continue
yield structure
except (AuthorizationError, APIError, socket.error) as err:
_LOGGER.error( _LOGGER.error(
"Connection error logging into the nest web service.") "Connection error while access Nest web service: %s", err)
def thermostats(self): def thermostats(self):
"""Generate a list of thermostats and their location.""" """Generate a list of thermostats."""
try: return self._devices('thermostats')
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.")
def smoke_co_alarms(self): def smoke_co_alarms(self):
"""Generate a list of smoke co alarms.""" """Generate a list of smoke co alarms."""
try: return self._devices('smoke_co_alarms')
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.")
def cameras(self): def cameras(self):
"""Generate a list of cameras.""" """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: try:
for structure in self.nest.structures: for structure in self.nest.structures:
if structure.name in self.local_structure: if structure.name not in self.local_structure:
for device in structure.cameras:
yield (structure, device)
else:
_LOGGER.debug("Ignoring structure %s, not in %s", _LOGGER.debug("Ignoring structure %s, not in %s",
structure.name, self.local_structure) 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( _LOGGER.error(
"Connection error logging into the nest web service.") "Connection error while access Nest web service: %s", err)
class NestSensorDevice(Entity): class NestSensorDevice(Entity):

View File

@ -24,10 +24,14 @@ PROTECT_SENSOR_TYPES = ['co_status',
# color_status: "gray", "green", "yellow", "red" # color_status: "gray", "green", "yellow", "red"
'color_status'] '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 \ _VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \
+ STRUCTURE_SENSOR_TYPES + STRUCTURE_SENSOR_TYPES + STRUCTURE_CAMERA_SENSOR_TYPES
SENSOR_UNITS = {'humidity': '%'} SENSOR_UNITS = {'humidity': '%'}
@ -105,6 +109,14 @@ async def async_setup_entry(hass, entry, async_add_devices):
for variable in conditions for variable in conditions
if variable in PROTECT_SENSOR_TYPES] 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 return all_sensors
async_add_devices(await hass.async_add_job(get_sensors), True) 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 \ elif self.variable in PROTECT_SENSOR_TYPES \
and self.variable != 'color_status': and self.variable != 'color_status':
# keep backward compatibility # 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: else:
self._state = getattr(self.device, self.variable) self._state = getattr(self.device, self.variable)

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

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

View File

@ -81,7 +81,7 @@ hbmqtt==0.9.2
holidays==0.9.5 holidays==0.9.5
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20180622.1 home-assistant-frontend==20180625.0
# homeassistant.components.influxdb # homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb # homeassistant.components.sensor.influxdb
@ -156,7 +156,7 @@ pyqwikswitch==0.8
python-forecastio==1.4.0 python-forecastio==1.4.0
# homeassistant.components.nest # homeassistant.components.nest
python-nest==4.0.2 python-nest==4.0.3
# homeassistant.components.sensor.whois # homeassistant.components.sensor.whois
pythonwhois==2.4.3 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.components.media_player import cast
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, mock_coro
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def cast_mock(): def cast_mock():
@ -359,3 +361,56 @@ async def test_disconnect_on_stop(hass: HomeAssistantType):
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done() await hass.async_block_till_done()
assert chromecast.disconnect.call_count == 1 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'}