mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
commit
d58e401812
@ -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])
|
||||||
|
@ -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']
|
||||||
|
@ -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,
|
||||||
|
@ -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."""
|
||||||
|
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:
|
if self.local_structure is None:
|
||||||
self.local_structure = [s.name for s in self.nest.structures]
|
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):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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'}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user