commit
d58e401812
|
@ -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])
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'}
|
||||
|
|
Loading…
Reference in New Issue