Axis config flow (#18543)
* Initial draft * Add tests for init Fix hound comments * Add tests for device Change parameter handling to make device easier to test * Remove superfluous functionality per Martins request * Fix hound comments * Embedded platforms * Fix device import * Config flow retry * Options default values will be set automatically to options in config entry before component can be used * Clean up init Add populate options Fix small issues in config flow Add tests covering init * Improve device tests * Add config flow tests * Fix hound comments * Rebase miss * Initial tests for binary sensors * Clean up More binary sensor tests * Hound comments * Add camera tests * Fix initial state of sensors * Bump dependency to v17 * Fix pylint and flake8 * Fix commentspull/22353/head
parent
9214934d47
commit
6988fe783c
|
@ -36,7 +36,6 @@ omit =
|
|||
homeassistant/components/arlo/*
|
||||
homeassistant/components/asterisk_mbox/*
|
||||
homeassistant/components/august/*
|
||||
homeassistant/components/axis/*
|
||||
homeassistant/components/bbb_gpio/*
|
||||
homeassistant/components/arest/binary_sensor.py
|
||||
homeassistant/components/concord232/binary_sensor.py
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "Axis device",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up Axis device",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"port": "Port"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Device is already configured",
|
||||
"device_unavailable": "Device is not available",
|
||||
"faulty_credentials": "Bad user credentials"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"bad_config_file": "Bad data from config file",
|
||||
"link_local_address": "Link local addresses are not supported"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,262 +1,76 @@
|
|||
"""Support for Axis devices."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.discovery import SERVICE_AXIS
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
ATTR_LOCATION, CONF_EVENT, CONF_HOST, CONF_INCLUDE,
|
||||
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TRIGGER_TIME, CONF_USERNAME,
|
||||
CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_TRIGGER_TIME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
REQUIREMENTS = ['axis==16']
|
||||
from .config_flow import configured_devices, DEVICE_SCHEMA
|
||||
from .const import CONF_CAMERA, CONF_EVENTS, DEFAULT_TRIGGER_TIME, DOMAIN
|
||||
from .device import AxisNetworkDevice, get_device
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'axis'
|
||||
CONFIG_FILE = 'axis.conf'
|
||||
|
||||
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
|
||||
'daynight', 'tampering', 'input']
|
||||
|
||||
PLATFORMS = ['camera']
|
||||
|
||||
AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
|
||||
|
||||
AXIS_DEFAULT_HOST = '192.168.0.90'
|
||||
AXIS_DEFAULT_USERNAME = 'root'
|
||||
AXIS_DEFAULT_PASSWORD = 'pass'
|
||||
DEFAULT_PORT = 80
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_INCLUDE):
|
||||
vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]),
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(ATTR_LOCATION, default=''): cv.string,
|
||||
})
|
||||
REQUIREMENTS = ['axis==17']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: cv.schema_with_slug_keys(DEVICE_SCHEMA),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SERVICE_VAPIX_CALL = 'vapix_call'
|
||||
SERVICE_VAPIX_CALL_RESPONSE = 'vapix_call_response'
|
||||
SERVICE_CGI = 'cgi'
|
||||
SERVICE_ACTION = 'action'
|
||||
SERVICE_PARAM = 'param'
|
||||
SERVICE_DEFAULT_CGI = 'param.cgi'
|
||||
SERVICE_DEFAULT_ACTION = 'update'
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(SERVICE_PARAM): cv.string,
|
||||
vol.Optional(SERVICE_CGI, default=SERVICE_DEFAULT_CGI): cv.string,
|
||||
vol.Optional(SERVICE_ACTION, default=SERVICE_DEFAULT_ACTION): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def request_configuration(hass, config, name, host, serialnumber):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
def configuration_callback(callback_data):
|
||||
"""Call when configuration is submitted."""
|
||||
if CONF_INCLUDE not in callback_data:
|
||||
configurator.notify_errors(
|
||||
request_id, "Functionality mandatory.")
|
||||
return False
|
||||
|
||||
callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split()
|
||||
callback_data[CONF_HOST] = host
|
||||
|
||||
if CONF_NAME not in callback_data:
|
||||
callback_data[CONF_NAME] = name
|
||||
|
||||
try:
|
||||
device_config = DEVICE_SCHEMA(callback_data)
|
||||
except vol.Invalid:
|
||||
configurator.notify_errors(
|
||||
request_id, "Bad input, please check spelling.")
|
||||
return False
|
||||
|
||||
if setup_device(hass, config, device_config):
|
||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||
config_file[serialnumber] = dict(device_config)
|
||||
save_json(hass.config.path(CONFIG_FILE), config_file)
|
||||
configurator.request_done(request_id)
|
||||
else:
|
||||
configurator.notify_errors(
|
||||
request_id, "Failed to register, please try again.")
|
||||
return False
|
||||
|
||||
title = '{} ({})'.format(name, host)
|
||||
request_id = configurator.request_config(
|
||||
title, configuration_callback,
|
||||
description='Functionality: ' + str(AXIS_INCLUDE),
|
||||
entity_picture="/static/images/logo_axis.png",
|
||||
link_name='Axis platform documentation',
|
||||
link_url='https://home-assistant.io/components/axis/',
|
||||
submit_caption="Confirm",
|
||||
fields=[
|
||||
{'id': CONF_NAME,
|
||||
'name': "Device name",
|
||||
'type': 'text'},
|
||||
{'id': CONF_USERNAME,
|
||||
'name': "User name",
|
||||
'type': 'text'},
|
||||
{'id': CONF_PASSWORD,
|
||||
'name': 'Password',
|
||||
'type': 'password'},
|
||||
{'id': CONF_INCLUDE,
|
||||
'name': "Device functionality (space separated list)",
|
||||
'type': 'text'},
|
||||
{'id': ATTR_LOCATION,
|
||||
'name': "Physical location of device (optional)",
|
||||
'type': 'text'},
|
||||
{'id': CONF_PORT,
|
||||
'name': "HTTP port (default=80)",
|
||||
'type': 'number'},
|
||||
{'id': CONF_TRIGGER_TIME,
|
||||
'name': "Sensor update interval (optional)",
|
||||
'type': 'number'},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up for Axis devices."""
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
def _shutdown(call):
|
||||
"""Stop the event stream on shutdown."""
|
||||
for serialnumber, device in hass.data[DOMAIN].items():
|
||||
_LOGGER.info("Stopping event stream for %s.", serialnumber)
|
||||
device.stop()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
|
||||
|
||||
def axis_device_discovered(service, discovery_info):
|
||||
"""Call when axis devices has been found."""
|
||||
host = discovery_info[CONF_HOST]
|
||||
name = discovery_info['hostname']
|
||||
serialnumber = discovery_info['properties']['macaddress']
|
||||
|
||||
if serialnumber not in hass.data[DOMAIN]:
|
||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||
if serialnumber in config_file:
|
||||
# Device config previously saved to file
|
||||
try:
|
||||
device_config = DEVICE_SCHEMA(config_file[serialnumber])
|
||||
device_config[CONF_HOST] = host
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
|
||||
return False
|
||||
if not setup_device(hass, config, device_config):
|
||||
_LOGGER.error(
|
||||
"Couldn't set up %s", device_config[CONF_NAME])
|
||||
else:
|
||||
# New device, create configuration request for UI
|
||||
request_configuration(hass, config, name, host, serialnumber)
|
||||
else:
|
||||
# Device already registered, but on a different IP
|
||||
device = hass.data[DOMAIN][serialnumber]
|
||||
device.config.host = host
|
||||
dispatcher_send(hass, DOMAIN + '_' + device.name + '_new_ip', host)
|
||||
|
||||
# Register discovery service
|
||||
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
|
||||
|
||||
if DOMAIN in config:
|
||||
for device in config[DOMAIN]:
|
||||
device_config = config[DOMAIN][device]
|
||||
|
||||
for device_name, device_config in config[DOMAIN].items():
|
||||
|
||||
if CONF_NAME not in device_config:
|
||||
device_config[CONF_NAME] = device
|
||||
if not setup_device(hass, config, device_config):
|
||||
_LOGGER.error("Couldn't set up %s", device_config[CONF_NAME])
|
||||
device_config[CONF_NAME] = device_name
|
||||
|
||||
def vapix_service(call):
|
||||
"""Service to send a message."""
|
||||
for device in hass.data[DOMAIN].values():
|
||||
if device.name == call.data[CONF_NAME]:
|
||||
response = device.vapix.do_request(
|
||||
call.data[SERVICE_CGI],
|
||||
call.data[SERVICE_ACTION],
|
||||
call.data[SERVICE_PARAM])
|
||||
hass.bus.fire(SERVICE_VAPIX_CALL_RESPONSE, response)
|
||||
return True
|
||||
_LOGGER.info("Couldn't find device %s", call.data[CONF_NAME])
|
||||
return False
|
||||
if device_config[CONF_HOST] not in configured_devices(hass):
|
||||
hass.async_create_task(hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={'source': config_entries.SOURCE_IMPORT},
|
||||
data=device_config
|
||||
))
|
||||
|
||||
# Register service with Home Assistant.
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_VAPIX_CALL, vapix_service, schema=SERVICE_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
def setup_device(hass, config, device_config):
|
||||
"""Set up an Axis device."""
|
||||
import axis
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
"""Set up the Axis component."""
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
def signal_callback(action, event):
|
||||
"""Call to configure events when initialized on event stream."""
|
||||
if action == 'add':
|
||||
event_config = {
|
||||
CONF_EVENT: event,
|
||||
CONF_NAME: device_config[CONF_NAME],
|
||||
ATTR_LOCATION: device_config[ATTR_LOCATION],
|
||||
CONF_TRIGGER_TIME: device_config[CONF_TRIGGER_TIME]
|
||||
}
|
||||
component = event.event_platform
|
||||
discovery.load_platform(
|
||||
hass, component, DOMAIN, event_config, config)
|
||||
if not config_entry.options:
|
||||
await async_populate_options(hass, config_entry)
|
||||
|
||||
event_types = [
|
||||
event
|
||||
for event in device_config[CONF_INCLUDE]
|
||||
if event in EVENT_TYPES
|
||||
]
|
||||
device = AxisNetworkDevice(hass, config_entry)
|
||||
|
||||
device = axis.AxisDevice(
|
||||
loop=hass.loop, host=device_config[CONF_HOST],
|
||||
username=device_config[CONF_USERNAME],
|
||||
password=device_config[CONF_PASSWORD],
|
||||
port=device_config[CONF_PORT], web_proto='http',
|
||||
event_types=event_types, signal=signal_callback)
|
||||
|
||||
try:
|
||||
hass.data[DOMAIN][device.vapix.serial_number] = device
|
||||
|
||||
except axis.Unauthorized:
|
||||
_LOGGER.error("Credentials for %s are faulty",
|
||||
device_config[CONF_HOST])
|
||||
if not await device.async_setup():
|
||||
return False
|
||||
|
||||
except axis.RequestError:
|
||||
return False
|
||||
hass.data[DOMAIN][device.serial] = device
|
||||
|
||||
device.name = device_config[CONF_NAME]
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown)
|
||||
|
||||
for component in device_config[CONF_INCLUDE]:
|
||||
if component == 'camera':
|
||||
camera_config = {
|
||||
CONF_NAME: device_config[CONF_NAME],
|
||||
CONF_HOST: device_config[CONF_HOST],
|
||||
CONF_PORT: device_config[CONF_PORT],
|
||||
CONF_USERNAME: device_config[CONF_USERNAME],
|
||||
CONF_PASSWORD: device_config[CONF_PASSWORD]
|
||||
}
|
||||
discovery.load_platform(
|
||||
hass, component, DOMAIN, camera_config, config)
|
||||
|
||||
if event_types:
|
||||
hass.add_job(device.start)
|
||||
return True
|
||||
|
||||
|
||||
async def async_populate_options(hass, config_entry):
|
||||
"""Populate default options for device."""
|
||||
from axis.vapix import VAPIX_IMAGE_FORMAT
|
||||
|
||||
device = await get_device(hass, config_entry.data[CONF_DEVICE])
|
||||
|
||||
supported_formats = device.vapix.get_param(VAPIX_IMAGE_FORMAT)
|
||||
|
||||
camera = bool(supported_formats)
|
||||
|
||||
options = {
|
||||
CONF_CAMERA: camera,
|
||||
CONF_EVENTS: True,
|
||||
CONF_TRIGGER_TIME: DEFAULT_TRIGGER_TIME
|
||||
}
|
||||
|
||||
hass.config_entries.async_update_entry(config_entry, options=options)
|
||||
|
|
|
@ -1,86 +1,87 @@
|
|||
"""Support for Axis binary sensors."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import (
|
||||
ATTR_LOCATION, CONF_EVENT, CONF_NAME, CONF_TRIGGER_TIME)
|
||||
from homeassistant.const import CONF_MAC, CONF_TRIGGER_TIME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
DEPENDENCIES = ['axis']
|
||||
from .const import DOMAIN as AXIS_DOMAIN, LOGGER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = [AXIS_DOMAIN]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Axis binary devices."""
|
||||
add_entities([AxisBinarySensor(discovery_info)], True)
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up a Axis binary sensor."""
|
||||
serial_number = config_entry.data[CONF_MAC]
|
||||
device = hass.data[AXIS_DOMAIN][serial_number]
|
||||
|
||||
@callback
|
||||
def async_add_sensor(event):
|
||||
"""Add binary sensor from Axis device."""
|
||||
async_add_entities([AxisBinarySensor(event, device)], True)
|
||||
|
||||
device.listeners.append(
|
||||
async_dispatcher_connect(hass, 'axis_add_sensor', async_add_sensor))
|
||||
|
||||
|
||||
class AxisBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a binary Axis event."""
|
||||
|
||||
def __init__(self, event_config):
|
||||
def __init__(self, event, device):
|
||||
"""Initialize the Axis binary sensor."""
|
||||
self.axis_event = event_config[CONF_EVENT]
|
||||
self.device_name = event_config[CONF_NAME]
|
||||
self.location = event_config[ATTR_LOCATION]
|
||||
self.delay = event_config[CONF_TRIGGER_TIME]
|
||||
self.event = event
|
||||
self.device = device
|
||||
self.delay = device.config_entry.options[CONF_TRIGGER_TIME]
|
||||
self.remove_timer = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe sensors events."""
|
||||
self.axis_event.callback = self._update_callback
|
||||
self.event.register_callback(self.update_callback)
|
||||
|
||||
def _update_callback(self):
|
||||
def update_callback(self):
|
||||
"""Update the sensor's state, if needed."""
|
||||
delay = self.device.config_entry.options[CONF_TRIGGER_TIME]
|
||||
|
||||
if self.remove_timer is not None:
|
||||
self.remove_timer()
|
||||
self.remove_timer = None
|
||||
|
||||
if self.delay == 0 or self.is_on:
|
||||
if delay == 0 or self.is_on:
|
||||
self.schedule_update_ha_state()
|
||||
else: # Run timer to delay updating the state
|
||||
@callback
|
||||
def _delay_update(now):
|
||||
"""Timer callback for sensor update."""
|
||||
_LOGGER.debug("%s called delayed (%s sec) update",
|
||||
self.name, self.delay)
|
||||
self.async_schedule_update_ha_state()
|
||||
self.remove_timer = None
|
||||
return
|
||||
|
||||
self.remove_timer = async_track_point_in_utc_time(
|
||||
self.hass, _delay_update,
|
||||
utcnow() + timedelta(seconds=self.delay))
|
||||
@callback
|
||||
def _delay_update(now):
|
||||
"""Timer callback for sensor update."""
|
||||
LOGGER.debug("%s called delayed (%s sec) update", self.name, delay)
|
||||
self.async_schedule_update_ha_state()
|
||||
self.remove_timer = None
|
||||
|
||||
self.remove_timer = async_track_point_in_utc_time(
|
||||
self.hass, _delay_update,
|
||||
utcnow() + timedelta(seconds=delay))
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if event is active."""
|
||||
return self.axis_event.is_tripped
|
||||
return self.event.is_tripped
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the event."""
|
||||
return '{}_{}_{}'.format(
|
||||
self.device_name, self.axis_event.event_type, self.axis_event.id)
|
||||
return '{} {} {}'.format(
|
||||
self.device.name, self.event.event_type, self.event.id)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the event."""
|
||||
return self.axis_event.event_class
|
||||
return self.event.event_class
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the event."""
|
||||
attr = {}
|
||||
|
||||
attr[ATTR_LOCATION] = self.location
|
||||
|
||||
return attr
|
||||
|
|
|
@ -1,58 +1,59 @@
|
|||
"""Support for Axis camera streaming."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.mjpeg.camera import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera, filter_urllib3_logging)
|
||||
from homeassistant.const import (
|
||||
CONF_AUTHENTICATION, CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT,
|
||||
CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.helpers.dispatcher import dispatcher_connect
|
||||
CONF_AUTHENTICATION, CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME,
|
||||
CONF_PASSWORD, CONF_PORT, CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import DOMAIN as AXIS_DOMAIN
|
||||
|
||||
DOMAIN = 'axis'
|
||||
DEPENDENCIES = [DOMAIN]
|
||||
DEPENDENCIES = [AXIS_DOMAIN]
|
||||
|
||||
AXIS_IMAGE = 'http://{}:{}/axis-cgi/jpg/image.cgi'
|
||||
AXIS_VIDEO = 'http://{}:{}/axis-cgi/mjpg/video.cgi'
|
||||
|
||||
|
||||
def _get_image_url(host, port, mode):
|
||||
"""Set the URL to get the image."""
|
||||
if mode == 'mjpeg':
|
||||
return 'http://{}:{}/axis-cgi/mjpg/video.cgi'.format(host, port)
|
||||
if mode == 'single':
|
||||
return 'http://{}:{}/axis-cgi/jpg/image.cgi'.format(host, port)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Axis camera."""
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Axis camera video stream."""
|
||||
filter_urllib3_logging()
|
||||
|
||||
camera_config = {
|
||||
CONF_NAME: discovery_info[CONF_NAME],
|
||||
CONF_USERNAME: discovery_info[CONF_USERNAME],
|
||||
CONF_PASSWORD: discovery_info[CONF_PASSWORD],
|
||||
CONF_MJPEG_URL: _get_image_url(
|
||||
discovery_info[CONF_HOST], str(discovery_info[CONF_PORT]),
|
||||
'mjpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(
|
||||
discovery_info[CONF_HOST], str(discovery_info[CONF_PORT]),
|
||||
'single'),
|
||||
serial_number = config_entry.data[CONF_MAC]
|
||||
device = hass.data[AXIS_DOMAIN][serial_number]
|
||||
|
||||
config = {
|
||||
CONF_NAME: config_entry.data[CONF_NAME],
|
||||
CONF_USERNAME: config_entry.data[CONF_DEVICE][CONF_USERNAME],
|
||||
CONF_PASSWORD: config_entry.data[CONF_DEVICE][CONF_PASSWORD],
|
||||
CONF_MJPEG_URL: AXIS_VIDEO.format(
|
||||
config_entry.data[CONF_DEVICE][CONF_HOST],
|
||||
config_entry.data[CONF_DEVICE][CONF_PORT]),
|
||||
CONF_STILL_IMAGE_URL: AXIS_IMAGE.format(
|
||||
config_entry.data[CONF_DEVICE][CONF_HOST],
|
||||
config_entry.data[CONF_DEVICE][CONF_PORT]),
|
||||
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
|
||||
}
|
||||
add_entities([AxisCamera(
|
||||
hass, camera_config, str(discovery_info[CONF_PORT]))])
|
||||
async_add_entities([AxisCamera(config, device)])
|
||||
|
||||
|
||||
class AxisCamera(MjpegCamera):
|
||||
"""Representation of a Axis camera."""
|
||||
|
||||
def __init__(self, hass, config, port):
|
||||
def __init__(self, config, device):
|
||||
"""Initialize Axis Communications camera component."""
|
||||
super().__init__(config)
|
||||
self.port = port
|
||||
dispatcher_connect(
|
||||
hass, DOMAIN + '_' + config[CONF_NAME] + '_new_ip', self._new_ip)
|
||||
self.device_config = config
|
||||
self.device = device
|
||||
self.port = device.config_entry.data[CONF_DEVICE][CONF_PORT]
|
||||
self.unsub_dispatcher = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe camera events."""
|
||||
self.unsub_dispatcher = async_dispatcher_connect(
|
||||
self.hass, 'axis_{}_new_ip'.format(self.device.name), self._new_ip)
|
||||
|
||||
def _new_ip(self, host):
|
||||
"""Set new IP for video stream."""
|
||||
self._mjpeg_url = _get_image_url(host, self.port, 'mjpeg')
|
||||
self._still_image_url = _get_image_url(host, self.port, 'single')
|
||||
self._mjpeg_url = AXIS_VIDEO.format(host, self.port)
|
||||
self._still_image_url = AXIS_IMAGE.format(host, self.port)
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
"""Config flow to configure Axis devices."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT,
|
||||
CONF_USERNAME)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util.json import load_json
|
||||
|
||||
from .const import CONF_MODEL, DOMAIN
|
||||
from .device import get_device
|
||||
from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect
|
||||
|
||||
CONFIG_FILE = 'axis.conf'
|
||||
|
||||
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
|
||||
'daynight', 'tampering', 'input']
|
||||
|
||||
PLATFORMS = ['camera']
|
||||
|
||||
AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
|
||||
|
||||
AXIS_DEFAULT_HOST = '192.168.0.90'
|
||||
AXIS_DEFAULT_USERNAME = 'root'
|
||||
AXIS_DEFAULT_PASSWORD = 'pass'
|
||||
DEFAULT_PORT = 80
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@callback
|
||||
def configured_devices(hass):
|
||||
"""Return a set of the configured devices."""
|
||||
return set(entry.data[CONF_DEVICE][CONF_HOST] for entry
|
||||
in hass.config_entries.async_entries(DOMAIN))
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class AxisFlowHandler(config_entries.ConfigFlow):
|
||||
"""Handle a Axis config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Axis config flow."""
|
||||
self.device_config = {}
|
||||
self.model = None
|
||||
self.name = None
|
||||
self.serial_number = None
|
||||
|
||||
self.discovery_schema = {}
|
||||
self.import_schema = {}
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a Axis config flow start.
|
||||
|
||||
Manage device specific parameters.
|
||||
"""
|
||||
from axis.vapix import VAPIX_MODEL_ID, VAPIX_SERIAL_NUMBER
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
if user_input[CONF_HOST] in configured_devices(self.hass):
|
||||
raise AlreadyConfigured
|
||||
|
||||
self.device_config = {
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD]
|
||||
}
|
||||
device = await get_device(self.hass, self.device_config)
|
||||
|
||||
self.serial_number = device.vapix.get_param(
|
||||
VAPIX_SERIAL_NUMBER)
|
||||
self.model = device.vapix.get_param(VAPIX_MODEL_ID)
|
||||
|
||||
return await self._create_entry()
|
||||
|
||||
except AlreadyConfigured:
|
||||
errors['base'] = 'already_configured'
|
||||
|
||||
except AuthenticationRequired:
|
||||
errors['base'] = 'faulty_credentials'
|
||||
|
||||
except CannotConnect:
|
||||
errors['base'] = 'device_unavailable'
|
||||
|
||||
data = self.import_schema or self.discovery_schema or {
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='user',
|
||||
description_placeholders=self.device_config,
|
||||
data_schema=vol.Schema(data),
|
||||
errors=errors
|
||||
)
|
||||
|
||||
async def _create_entry(self):
|
||||
"""Create entry for device.
|
||||
|
||||
Generate a name to be used as a prefix for device entities.
|
||||
"""
|
||||
if self.name is None:
|
||||
same_model = [
|
||||
entry.data[CONF_NAME] for entry
|
||||
in self.hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.data[CONF_MODEL] == self.model
|
||||
]
|
||||
|
||||
self.name = "{}".format(self.model)
|
||||
for idx in range(len(same_model) + 1):
|
||||
self.name = "{} {}".format(self.model, idx)
|
||||
if self.name not in same_model:
|
||||
break
|
||||
|
||||
data = {
|
||||
CONF_DEVICE: self.device_config,
|
||||
CONF_NAME: self.name,
|
||||
CONF_MAC: self.serial_number,
|
||||
CONF_MODEL: self.model,
|
||||
}
|
||||
|
||||
title = "{} - {}".format(self.model, self.serial_number)
|
||||
return self.async_create_entry(
|
||||
title=title,
|
||||
data=data
|
||||
)
|
||||
|
||||
async def async_step_discovery(self, discovery_info):
|
||||
"""Prepare configuration for a discovered Axis device.
|
||||
|
||||
This flow is triggered by the discovery component.
|
||||
"""
|
||||
if discovery_info[CONF_HOST] in configured_devices(self.hass):
|
||||
return self.async_abort(reason='already_configured')
|
||||
|
||||
if discovery_info[CONF_HOST].startswith('169.254'):
|
||||
return self.async_abort(reason='link_local_address')
|
||||
|
||||
config_file = await self.hass.async_add_executor_job(
|
||||
load_json, self.hass.config.path(CONFIG_FILE))
|
||||
|
||||
serialnumber = discovery_info['properties']['macaddress']
|
||||
|
||||
if serialnumber not in config_file:
|
||||
self.discovery_schema = {
|
||||
vol.Required(
|
||||
CONF_HOST, default=discovery_info[CONF_HOST]): str,
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_PORT, default=discovery_info[CONF_PORT]): int
|
||||
}
|
||||
return await self.async_step_user()
|
||||
|
||||
try:
|
||||
device_config = DEVICE_SCHEMA(config_file[serialnumber])
|
||||
device_config[CONF_HOST] = discovery_info[CONF_HOST]
|
||||
|
||||
if CONF_NAME not in device_config:
|
||||
device_config[CONF_NAME] = discovery_info['hostname']
|
||||
|
||||
except vol.Invalid:
|
||||
return self.async_abort(reason='bad_config_file')
|
||||
|
||||
return await self.async_step_import(device_config)
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
"""Import a Axis device as a config entry.
|
||||
|
||||
This flow is triggered by `async_setup` for configured devices.
|
||||
This flow is also triggered by `async_step_discovery`.
|
||||
|
||||
This will execute for any Axis device that contains a complete
|
||||
configuration.
|
||||
"""
|
||||
self.name = import_config[CONF_NAME]
|
||||
|
||||
self.import_schema = {
|
||||
vol.Required(CONF_HOST, default=import_config[CONF_HOST]): str,
|
||||
vol.Required(
|
||||
CONF_USERNAME, default=import_config[CONF_USERNAME]): str,
|
||||
vol.Required(
|
||||
CONF_PASSWORD, default=import_config[CONF_PASSWORD]): str,
|
||||
vol.Required(CONF_PORT, default=import_config[CONF_PORT]): int
|
||||
}
|
||||
return await self.async_step_user(user_input=import_config)
|
|
@ -0,0 +1,12 @@
|
|||
"""Constants for the Axis component."""
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger('homeassistant.components.axis')
|
||||
|
||||
DOMAIN = 'axis'
|
||||
|
||||
CONF_CAMERA = 'camera'
|
||||
CONF_EVENTS = 'events'
|
||||
CONF_MODEL = 'model'
|
||||
|
||||
DEFAULT_TRIGGER_TIME = 0
|
|
@ -0,0 +1,127 @@
|
|||
"""Axis network device abstraction."""
|
||||
|
||||
import asyncio
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT,
|
||||
CONF_USERNAME)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import CONF_CAMERA, CONF_EVENTS, CONF_MODEL, LOGGER
|
||||
from .errors import AuthenticationRequired, CannotConnect
|
||||
|
||||
|
||||
class AxisNetworkDevice:
|
||||
"""Manages a Axis device."""
|
||||
|
||||
def __init__(self, hass, config_entry):
|
||||
"""Initialize the device."""
|
||||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
self.available = True
|
||||
|
||||
self.api = None
|
||||
self.fw_version = None
|
||||
self.product_type = None
|
||||
|
||||
self.listeners = []
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
"""Return the host of this device."""
|
||||
return self.config_entry.data[CONF_DEVICE][CONF_HOST]
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return the model of this device."""
|
||||
return self.config_entry.data[CONF_MODEL]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self.config_entry.data[CONF_NAME]
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
"""Return the mac of this device."""
|
||||
return self.config_entry.data[CONF_MAC]
|
||||
|
||||
async def async_setup(self):
|
||||
"""Set up the device."""
|
||||
from axis.vapix import VAPIX_FW_VERSION, VAPIX_PROD_TYPE
|
||||
|
||||
hass = self.hass
|
||||
|
||||
try:
|
||||
self.api = await get_device(
|
||||
hass, self.config_entry.data[CONF_DEVICE],
|
||||
event_types='on', signal_callback=self.async_signal_callback)
|
||||
|
||||
except CannotConnect:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
LOGGER.error(
|
||||
'Unknown error connecting with Axis device on %s', self.host)
|
||||
return False
|
||||
|
||||
self.fw_version = self.api.vapix.get_param(VAPIX_FW_VERSION)
|
||||
self.product_type = self.api.vapix.get_param(VAPIX_PROD_TYPE)
|
||||
|
||||
if self.config_entry.options[CONF_CAMERA]:
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_forward_entry_setup(
|
||||
self.config_entry, 'camera'))
|
||||
|
||||
if self.config_entry.options[CONF_EVENTS]:
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_forward_entry_setup(
|
||||
self.config_entry, 'binary_sensor'))
|
||||
self.api.start()
|
||||
|
||||
return True
|
||||
|
||||
@callback
|
||||
def async_signal_callback(self, action, event):
|
||||
"""Call to configure events when initialized on event stream."""
|
||||
if action == 'add':
|
||||
async_dispatcher_send(self.hass, 'axis_add_sensor', event)
|
||||
|
||||
@callback
|
||||
def shutdown(self, event):
|
||||
"""Stop the event stream."""
|
||||
self.api.stop()
|
||||
|
||||
|
||||
async def get_device(hass, config, event_types=None, signal_callback=None):
|
||||
"""Create a Axis device."""
|
||||
import axis
|
||||
|
||||
device = axis.AxisDevice(
|
||||
loop=hass.loop, host=config[CONF_HOST],
|
||||
username=config[CONF_USERNAME],
|
||||
password=config[CONF_PASSWORD],
|
||||
port=config[CONF_PORT], web_proto='http',
|
||||
event_types=event_types, signal=signal_callback)
|
||||
|
||||
try:
|
||||
with async_timeout.timeout(15):
|
||||
await hass.async_add_executor_job(device.vapix.load_params)
|
||||
return device
|
||||
|
||||
except axis.Unauthorized:
|
||||
LOGGER.warning("Connected to device at %s but not registered.",
|
||||
config[CONF_HOST])
|
||||
raise AuthenticationRequired
|
||||
|
||||
except (asyncio.TimeoutError, axis.RequestError):
|
||||
LOGGER.error("Error connecting to the Axis device at %s",
|
||||
config[CONF_HOST])
|
||||
raise CannotConnect
|
||||
|
||||
except axis.AxisException:
|
||||
LOGGER.exception('Unknown Axis communication error occurred')
|
||||
raise AuthenticationRequired
|
|
@ -0,0 +1,22 @@
|
|||
"""Errors for the Axis component."""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
|
||||
class AxisException(HomeAssistantError):
|
||||
"""Base class for Axis exceptions."""
|
||||
|
||||
|
||||
class AlreadyConfigured(AxisException):
|
||||
"""Device is already configured."""
|
||||
|
||||
|
||||
class AuthenticationRequired(AxisException):
|
||||
"""Unknown error occurred."""
|
||||
|
||||
|
||||
class CannotConnect(AxisException):
|
||||
"""Unable to connect to the device."""
|
||||
|
||||
|
||||
class UserLevel(AxisException):
|
||||
"""User level too low."""
|
|
@ -1,15 +0,0 @@
|
|||
vapix_call:
|
||||
description: Configure device using Vapix parameter management.
|
||||
fields:
|
||||
name:
|
||||
description: Name of device to Configure. [Required]
|
||||
example: M1065-W
|
||||
cgi:
|
||||
description: Which cgi to call on device. [Optional] Default is 'param.cgi'
|
||||
example: 'applications/control.cgi'
|
||||
action:
|
||||
description: What type of call. [Optional] Default is 'update'
|
||||
example: 'start'
|
||||
param:
|
||||
description: What parameter to operate on. [Required]
|
||||
example: 'package=VideoMotionDetection'
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "Axis device",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up Axis device",
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"port": "Port"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Device is already configured",
|
||||
"device_unavailable": "Device is not available",
|
||||
"faulty_credentials": "Bad user credentials"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"bad_config_file": "Bad data from config file",
|
||||
"link_local_address": "Link local addresses are not supported"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,6 +52,7 @@ SERVICE_WINK = 'wink'
|
|||
SERVICE_XIAOMI_GW = 'xiaomi_gw'
|
||||
|
||||
CONFIG_ENTRY_HANDLERS = {
|
||||
SERVICE_AXIS: 'axis',
|
||||
SERVICE_DAIKIN: 'daikin',
|
||||
SERVICE_DECONZ: 'deconz',
|
||||
'esphome': 'esphome',
|
||||
|
@ -69,7 +70,6 @@ SERVICE_HANDLERS = {
|
|||
SERVICE_NETGEAR: ('device_tracker', None),
|
||||
SERVICE_WEMO: ('wemo', None),
|
||||
SERVICE_HASSIO: ('hassio', None),
|
||||
SERVICE_AXIS: ('axis', None),
|
||||
SERVICE_APPLE_TV: ('apple_tv', None),
|
||||
SERVICE_ENIGMA2: ('media_player', 'enigma2'),
|
||||
SERVICE_ROKU: ('roku', None),
|
||||
|
|
|
@ -143,6 +143,7 @@ HANDLERS = Registry()
|
|||
# Components that have config flows. In future we will auto-generate this list.
|
||||
FLOWS = [
|
||||
'ambient_station',
|
||||
'axis',
|
||||
'cast',
|
||||
'daikin',
|
||||
'deconz',
|
||||
|
|
|
@ -186,7 +186,7 @@ av==6.1.2
|
|||
# avion==0.10
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==16
|
||||
axis==17
|
||||
|
||||
# homeassistant.components.tts.baidu
|
||||
baidu-aip==1.6.6
|
||||
|
|
|
@ -56,6 +56,9 @@ apns2==0.3.0
|
|||
# homeassistant.components.stream
|
||||
av==6.1.2
|
||||
|
||||
# homeassistant.components.axis
|
||||
axis==17
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows-homeassistant==0.7.1
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ TEST_REQUIREMENTS = (
|
|||
'aiounifi',
|
||||
'apns2',
|
||||
'av',
|
||||
'axis',
|
||||
'caldav',
|
||||
'coinmarketcap',
|
||||
'defusedxml',
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Axis component."""
|
|
@ -0,0 +1,102 @@
|
|||
"""Axis binary sensor platform tests."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import axis
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
import homeassistant.components.binary_sensor as binary_sensor
|
||||
|
||||
EVENTS = [
|
||||
{
|
||||
'operation': 'Initialized',
|
||||
'topic': 'tns1:Device/tnsaxis:Sensor/PIR',
|
||||
'source': 'sensor',
|
||||
'source_idx': '0',
|
||||
'type': 'state',
|
||||
'value': '0'
|
||||
},
|
||||
{
|
||||
'operation': 'Initialized',
|
||||
'topic': 'tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1',
|
||||
'type': 'active',
|
||||
'value': '1'
|
||||
}
|
||||
]
|
||||
|
||||
ENTRY_CONFIG = {
|
||||
axis.CONF_DEVICE: {
|
||||
axis.config_flow.CONF_HOST: '1.2.3.4',
|
||||
axis.config_flow.CONF_USERNAME: 'user',
|
||||
axis.config_flow.CONF_PASSWORD: 'pass',
|
||||
axis.config_flow.CONF_PORT: 80
|
||||
},
|
||||
axis.config_flow.CONF_MAC: '1234ABCD',
|
||||
axis.config_flow.CONF_MODEL: 'model',
|
||||
axis.config_flow.CONF_NAME: 'model 0'
|
||||
}
|
||||
|
||||
ENTRY_OPTIONS = {
|
||||
axis.CONF_CAMERA: False,
|
||||
axis.CONF_EVENTS: True,
|
||||
axis.CONF_TRIGGER_TIME: 0
|
||||
}
|
||||
|
||||
|
||||
async def setup_device(hass):
|
||||
"""Load the Axis binary sensor platform."""
|
||||
from axis import AxisDevice
|
||||
loop = Mock()
|
||||
|
||||
config_entry = config_entries.ConfigEntry(
|
||||
1, axis.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test',
|
||||
config_entries.CONN_CLASS_LOCAL_PUSH, options=ENTRY_OPTIONS)
|
||||
device = axis.AxisNetworkDevice(hass, config_entry)
|
||||
device.api = AxisDevice(loop=loop, **config_entry.data[axis.CONF_DEVICE],
|
||||
signal=device.async_signal_callback)
|
||||
hass.data[axis.DOMAIN] = {device.serial: device}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setup(
|
||||
config_entry, 'binary_sensor')
|
||||
# To flush out the service call to update the group
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return device
|
||||
|
||||
|
||||
async def test_platform_manually_configured(hass):
|
||||
"""Test that nothing happens when platform is manually configured."""
|
||||
assert await async_setup_component(hass, binary_sensor.DOMAIN, {
|
||||
'binary_sensor': {
|
||||
'platform': axis.DOMAIN
|
||||
}
|
||||
}) is True
|
||||
|
||||
assert axis.DOMAIN not in hass.data
|
||||
|
||||
|
||||
async def test_no_binary_sensors(hass):
|
||||
"""Test that no sensors in Axis results in no sensor entities."""
|
||||
await setup_device(hass)
|
||||
|
||||
assert len(hass.states.async_all()) == 0
|
||||
|
||||
|
||||
async def test_binary_sensors(hass):
|
||||
"""Test that sensors are loaded properly."""
|
||||
device = await setup_device(hass)
|
||||
|
||||
for event in EVENTS:
|
||||
device.api.stream.event.manage_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 2
|
||||
|
||||
pir = hass.states.get('binary_sensor.model_0_pir_0')
|
||||
assert pir.state == 'off'
|
||||
assert pir.name == 'model 0 PIR 0'
|
||||
|
||||
vmd4 = hass.states.get('binary_sensor.model_0_vmd4_camera1profile1')
|
||||
assert vmd4.state == 'on'
|
||||
assert vmd4.name == 'model 0 VMD4 Camera1Profile1'
|
|
@ -0,0 +1,73 @@
|
|||
"""Axis camera platform tests."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import axis
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
import homeassistant.components.camera as camera
|
||||
|
||||
|
||||
ENTRY_CONFIG = {
|
||||
axis.CONF_DEVICE: {
|
||||
axis.config_flow.CONF_HOST: '1.2.3.4',
|
||||
axis.config_flow.CONF_USERNAME: 'user',
|
||||
axis.config_flow.CONF_PASSWORD: 'pass',
|
||||
axis.config_flow.CONF_PORT: 80
|
||||
},
|
||||
axis.config_flow.CONF_MAC: '1234ABCD',
|
||||
axis.config_flow.CONF_MODEL: 'model',
|
||||
axis.config_flow.CONF_NAME: 'model 0'
|
||||
}
|
||||
|
||||
ENTRY_OPTIONS = {
|
||||
axis.CONF_CAMERA: False,
|
||||
axis.CONF_EVENTS: True,
|
||||
axis.CONF_TRIGGER_TIME: 0
|
||||
}
|
||||
|
||||
|
||||
async def setup_device(hass):
|
||||
"""Load the Axis binary sensor platform."""
|
||||
from axis import AxisDevice
|
||||
loop = Mock()
|
||||
|
||||
config_entry = config_entries.ConfigEntry(
|
||||
1, axis.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test',
|
||||
config_entries.CONN_CLASS_LOCAL_PUSH, options=ENTRY_OPTIONS)
|
||||
device = axis.AxisNetworkDevice(hass, config_entry)
|
||||
device.api = AxisDevice(loop=loop, **config_entry.data[axis.CONF_DEVICE],
|
||||
signal=device.async_signal_callback)
|
||||
hass.data[axis.DOMAIN] = {device.serial: device}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setup(
|
||||
config_entry, 'camera')
|
||||
# To flush out the service call to update the group
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return device
|
||||
|
||||
|
||||
async def test_platform_manually_configured(hass):
|
||||
"""Test that nothing happens when platform is manually configured."""
|
||||
assert await async_setup_component(hass, camera.DOMAIN, {
|
||||
'camera': {
|
||||
'platform': axis.DOMAIN
|
||||
}
|
||||
}) is True
|
||||
|
||||
assert axis.DOMAIN not in hass.data
|
||||
|
||||
|
||||
async def test_camera(hass):
|
||||
"""Test that Axis camera platform is loaded properly."""
|
||||
await setup_device(hass)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
cam = hass.states.get('camera.model_0')
|
||||
assert cam.state == 'idle'
|
||||
assert cam.name == 'model 0'
|
|
@ -0,0 +1,319 @@
|
|||
"""Test Axis config flow."""
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homeassistant.components import axis
|
||||
from homeassistant.components.axis import config_flow
|
||||
|
||||
from tests.common import mock_coro, MockConfigEntry
|
||||
|
||||
import axis as axis_lib
|
||||
|
||||
|
||||
async def test_configured_devices(hass):
|
||||
"""Test that configured devices works as expected."""
|
||||
result = config_flow.configured_devices(hass)
|
||||
|
||||
assert not result
|
||||
|
||||
entry = MockConfigEntry(domain=axis.DOMAIN,
|
||||
data={axis.CONF_DEVICE: {axis.CONF_HOST: ''}})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = config_flow.configured_devices(hass)
|
||||
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
async def test_flow_works(hass):
|
||||
"""Test that config flow works."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch('axis.AxisDevice') as mock_device:
|
||||
def mock_constructor(
|
||||
loop, host, username, password, port, web_proto, event_types,
|
||||
signal):
|
||||
"""Fake the controller constructor."""
|
||||
mock_device.loop = loop
|
||||
mock_device.host = host
|
||||
mock_device.username = username
|
||||
mock_device.password = password
|
||||
mock_device.port = port
|
||||
return mock_device
|
||||
|
||||
def mock_get_param(param):
|
||||
"""Fake get param method."""
|
||||
return param
|
||||
|
||||
mock_device.side_effect = mock_constructor
|
||||
mock_device.vapix.load_params.return_value = Mock()
|
||||
mock_device.vapix.get_param.side_effect = mock_get_param
|
||||
|
||||
result = await flow.async_step_user(user_input={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_USERNAME: 'user',
|
||||
config_flow.CONF_PASSWORD: 'pass',
|
||||
config_flow.CONF_PORT: 81
|
||||
})
|
||||
|
||||
assert result['type'] == 'create_entry'
|
||||
assert result['title'] == '{} - {}'.format(
|
||||
axis_lib.vapix.VAPIX_MODEL_ID, axis_lib.vapix.VAPIX_SERIAL_NUMBER)
|
||||
assert result['data'] == {
|
||||
axis.CONF_DEVICE: {
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_USERNAME: 'user',
|
||||
config_flow.CONF_PASSWORD: 'pass',
|
||||
config_flow.CONF_PORT: 81
|
||||
},
|
||||
config_flow.CONF_MAC: axis_lib.vapix.VAPIX_SERIAL_NUMBER,
|
||||
config_flow.CONF_MODEL: axis_lib.vapix.VAPIX_MODEL_ID,
|
||||
config_flow.CONF_NAME: 'Brand.ProdNbr 0'
|
||||
}
|
||||
|
||||
|
||||
async def test_flow_fails_already_configured(hass):
|
||||
"""Test that config flow fails on already configured device."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
entry = MockConfigEntry(domain=axis.DOMAIN, data={axis.CONF_DEVICE: {
|
||||
axis.CONF_HOST: '1.2.3.4'
|
||||
}})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await flow.async_step_user(user_input={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_USERNAME: 'user',
|
||||
config_flow.CONF_PASSWORD: 'pass',
|
||||
config_flow.CONF_PORT: 81
|
||||
})
|
||||
|
||||
assert result['errors'] == {'base': 'already_configured'}
|
||||
|
||||
|
||||
async def test_flow_fails_faulty_credentials(hass):
|
||||
"""Test that config flow fails on faulty credentials."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch('homeassistant.components.axis.config_flow.get_device',
|
||||
side_effect=config_flow.AuthenticationRequired):
|
||||
result = await flow.async_step_user(user_input={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_USERNAME: 'user',
|
||||
config_flow.CONF_PASSWORD: 'pass',
|
||||
config_flow.CONF_PORT: 81
|
||||
})
|
||||
|
||||
assert result['errors'] == {'base': 'faulty_credentials'}
|
||||
|
||||
|
||||
async def test_flow_fails_device_unavailable(hass):
|
||||
"""Test that config flow fails on device unavailable."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch('homeassistant.components.axis.config_flow.get_device',
|
||||
side_effect=config_flow.CannotConnect):
|
||||
result = await flow.async_step_user(user_input={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_USERNAME: 'user',
|
||||
config_flow.CONF_PASSWORD: 'pass',
|
||||
config_flow.CONF_PORT: 81
|
||||
})
|
||||
|
||||
assert result['errors'] == {'base': 'device_unavailable'}
|
||||
|
||||
|
||||
async def test_flow_create_entry(hass):
|
||||
"""Test that create entry can generate a name without other entries."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.model = 'model'
|
||||
|
||||
result = await flow._create_entry()
|
||||
|
||||
assert result['data'][config_flow.CONF_NAME] == 'model 0'
|
||||
|
||||
|
||||
async def test_flow_create_entry_more_entries(hass):
|
||||
"""Test that create entry can generate a name with other entries."""
|
||||
entry = MockConfigEntry(
|
||||
domain=axis.DOMAIN, data={config_flow.CONF_NAME: 'model 0',
|
||||
config_flow.CONF_MODEL: 'model'})
|
||||
entry.add_to_hass(hass)
|
||||
entry2 = MockConfigEntry(
|
||||
domain=axis.DOMAIN, data={config_flow.CONF_NAME: 'model 1',
|
||||
config_flow.CONF_MODEL: 'model'})
|
||||
entry2.add_to_hass(hass)
|
||||
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.model = 'model'
|
||||
|
||||
result = await flow._create_entry()
|
||||
|
||||
assert result['data'][config_flow.CONF_NAME] == 'model 2'
|
||||
|
||||
|
||||
async def test_discovery_flow(hass):
|
||||
"""Test that discovery for new devices work."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch.object(axis, 'get_device', return_value=mock_coro(Mock())):
|
||||
result = await flow.async_step_discovery(discovery_info={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_PORT: 80,
|
||||
'properties': {'macaddress': '1234'}
|
||||
})
|
||||
|
||||
assert result['type'] == 'form'
|
||||
assert result['step_id'] == 'user'
|
||||
|
||||
|
||||
async def test_discovery_flow_known_device(hass):
|
||||
"""Test that discovery for known devices work.
|
||||
|
||||
This is legacy support from devices registered with configurator.
|
||||
"""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch('homeassistant.components.axis.config_flow.load_json',
|
||||
return_value={'1234ABCD': {
|
||||
config_flow.CONF_HOST: '2.3.4.5',
|
||||
config_flow.CONF_USERNAME: 'user',
|
||||
config_flow.CONF_PASSWORD: 'pass',
|
||||
config_flow.CONF_PORT: 80}}), \
|
||||
patch('axis.AxisDevice') as mock_device:
|
||||
def mock_constructor(
|
||||
loop, host, username, password, port, web_proto, event_types,
|
||||
signal):
|
||||
"""Fake the controller constructor."""
|
||||
mock_device.loop = loop
|
||||
mock_device.host = host
|
||||
mock_device.username = username
|
||||
mock_device.password = password
|
||||
mock_device.port = port
|
||||
return mock_device
|
||||
|
||||
def mock_get_param(param):
|
||||
"""Fake get param method."""
|
||||
return param
|
||||
|
||||
mock_device.side_effect = mock_constructor
|
||||
mock_device.vapix.load_params.return_value = Mock()
|
||||
mock_device.vapix.get_param.side_effect = mock_get_param
|
||||
|
||||
result = await flow.async_step_discovery(discovery_info={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_PORT: 80,
|
||||
'hostname': 'name',
|
||||
'properties': {'macaddress': '1234ABCD'}
|
||||
})
|
||||
|
||||
assert result['type'] == 'create_entry'
|
||||
|
||||
|
||||
async def test_discovery_flow_already_configured(hass):
|
||||
"""Test that discovery doesn't setup already configured devices."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
entry = MockConfigEntry(domain=axis.DOMAIN, data={axis.CONF_DEVICE: {
|
||||
axis.CONF_HOST: '1.2.3.4'
|
||||
}})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await flow.async_step_discovery(discovery_info={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_USERNAME: 'user',
|
||||
config_flow.CONF_PASSWORD: 'pass',
|
||||
config_flow.CONF_PORT: 81
|
||||
})
|
||||
print(result)
|
||||
assert result['type'] == 'abort'
|
||||
|
||||
|
||||
async def test_discovery_flow_link_local_address(hass):
|
||||
"""Test that discovery doesn't setup devices with link local addresses."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
result = await flow.async_step_discovery(discovery_info={
|
||||
config_flow.CONF_HOST: '169.254.3.4'
|
||||
})
|
||||
|
||||
assert result['type'] == 'abort'
|
||||
|
||||
|
||||
async def test_discovery_flow_bad_config_file(hass):
|
||||
"""Test that discovery with bad config files abort."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch('homeassistant.components.axis.config_flow.load_json',
|
||||
return_value={'1234ABCD': {
|
||||
config_flow.CONF_HOST: '2.3.4.5',
|
||||
config_flow.CONF_USERNAME: 'user',
|
||||
config_flow.CONF_PASSWORD: 'pass',
|
||||
config_flow.CONF_PORT: 80}}), \
|
||||
patch('homeassistant.components.axis.config_flow.DEVICE_SCHEMA',
|
||||
side_effect=config_flow.vol.Invalid('')):
|
||||
result = await flow.async_step_discovery(discovery_info={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
'properties': {'macaddress': '1234ABCD'}
|
||||
})
|
||||
|
||||
assert result['type'] == 'abort'
|
||||
|
||||
|
||||
async def test_import_flow_works(hass):
|
||||
"""Test that import flow works."""
|
||||
flow = config_flow.AxisFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch('axis.AxisDevice') as mock_device:
|
||||
def mock_constructor(
|
||||
loop, host, username, password, port, web_proto, event_types,
|
||||
signal):
|
||||
"""Fake the controller constructor."""
|
||||
mock_device.loop = loop
|
||||
mock_device.host = host
|
||||
mock_device.username = username
|
||||
mock_device.password = password
|
||||
mock_device.port = port
|
||||
return mock_device
|
||||
|
||||
def mock_get_param(param):
|
||||
"""Fake get param method."""
|
||||
return param
|
||||
|
||||
mock_device.side_effect = mock_constructor
|
||||
mock_device.vapix.load_params.return_value = Mock()
|
||||
mock_device.vapix.get_param.side_effect = mock_get_param
|
||||
|
||||
result = await flow.async_step_import(import_config={
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_USERNAME: 'user',
|
||||
config_flow.CONF_PASSWORD: 'pass',
|
||||
config_flow.CONF_PORT: 81,
|
||||
config_flow.CONF_NAME: 'name'
|
||||
})
|
||||
|
||||
assert result['type'] == 'create_entry'
|
||||
assert result['title'] == '{} - {}'.format(
|
||||
axis_lib.vapix.VAPIX_MODEL_ID, axis_lib.vapix.VAPIX_SERIAL_NUMBER)
|
||||
assert result['data'] == {
|
||||
axis.CONF_DEVICE: {
|
||||
config_flow.CONF_HOST: '1.2.3.4',
|
||||
config_flow.CONF_USERNAME: 'user',
|
||||
config_flow.CONF_PASSWORD: 'pass',
|
||||
config_flow.CONF_PORT: 81
|
||||
},
|
||||
config_flow.CONF_MAC: axis_lib.vapix.VAPIX_SERIAL_NUMBER,
|
||||
config_flow.CONF_MODEL: axis_lib.vapix.VAPIX_MODEL_ID,
|
||||
config_flow.CONF_NAME: 'name'
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
"""Test Axis device."""
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.common import mock_coro
|
||||
|
||||
from homeassistant.components.axis import device, errors
|
||||
|
||||
DEVICE_DATA = {
|
||||
device.CONF_HOST: '1.2.3.4',
|
||||
device.CONF_USERNAME: 'username',
|
||||
device.CONF_PASSWORD: 'password',
|
||||
device.CONF_PORT: 1234
|
||||
}
|
||||
|
||||
ENTRY_OPTIONS = {
|
||||
device.CONF_CAMERA: True,
|
||||
device.CONF_EVENTS: ['pir'],
|
||||
}
|
||||
|
||||
ENTRY_CONFIG = {
|
||||
device.CONF_DEVICE: DEVICE_DATA,
|
||||
device.CONF_MAC: 'mac',
|
||||
device.CONF_MODEL: 'model',
|
||||
device.CONF_NAME: 'name'
|
||||
}
|
||||
|
||||
|
||||
async def test_device_setup():
|
||||
"""Successful setup."""
|
||||
hass = Mock()
|
||||
entry = Mock()
|
||||
entry.data = ENTRY_CONFIG
|
||||
entry.options = ENTRY_OPTIONS
|
||||
api = Mock()
|
||||
|
||||
axis_device = device.AxisNetworkDevice(hass, entry)
|
||||
|
||||
assert axis_device.host == DEVICE_DATA[device.CONF_HOST]
|
||||
assert axis_device.model == ENTRY_CONFIG[device.CONF_MODEL]
|
||||
assert axis_device.name == ENTRY_CONFIG[device.CONF_NAME]
|
||||
assert axis_device.serial == ENTRY_CONFIG[device.CONF_MAC]
|
||||
|
||||
with patch.object(device, 'get_device', return_value=mock_coro(api)):
|
||||
assert await axis_device.async_setup() is True
|
||||
|
||||
assert axis_device.api is api
|
||||
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 2
|
||||
assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \
|
||||
(entry, 'camera')
|
||||
assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == \
|
||||
(entry, 'binary_sensor')
|
||||
|
||||
|
||||
async def test_device_not_accessible():
|
||||
"""Failed setup schedules a retry of setup."""
|
||||
hass = Mock()
|
||||
hass.data = dict()
|
||||
entry = Mock()
|
||||
entry.data = ENTRY_CONFIG
|
||||
entry.options = ENTRY_OPTIONS
|
||||
|
||||
axis_device = device.AxisNetworkDevice(hass, entry)
|
||||
|
||||
with patch.object(device, 'get_device',
|
||||
side_effect=errors.CannotConnect), \
|
||||
pytest.raises(device.ConfigEntryNotReady):
|
||||
await axis_device.async_setup()
|
||||
|
||||
assert not hass.helpers.event.async_call_later.mock_calls
|
||||
|
||||
|
||||
async def test_device_unknown_error():
|
||||
"""Unknown errors are handled."""
|
||||
hass = Mock()
|
||||
entry = Mock()
|
||||
entry.data = ENTRY_CONFIG
|
||||
entry.options = ENTRY_OPTIONS
|
||||
|
||||
axis_device = device.AxisNetworkDevice(hass, entry)
|
||||
|
||||
with patch.object(device, 'get_device', side_effect=Exception):
|
||||
assert await axis_device.async_setup() is False
|
||||
|
||||
assert not hass.helpers.event.async_call_later.mock_calls
|
||||
|
||||
|
||||
async def test_new_event_sends_signal(hass):
|
||||
"""Make sure that new event send signal."""
|
||||
entry = Mock()
|
||||
entry.data = ENTRY_CONFIG
|
||||
|
||||
axis_device = device.AxisNetworkDevice(hass, entry)
|
||||
|
||||
with patch.object(device, 'async_dispatcher_send') as mock_dispatch_send:
|
||||
axis_device.async_signal_callback(action='add', event='event')
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_dispatch_send.mock_calls) == 1
|
||||
assert len(mock_dispatch_send.mock_calls[0]) == 3
|
||||
|
||||
|
||||
async def test_shutdown():
|
||||
"""Successful shutdown."""
|
||||
hass = Mock()
|
||||
entry = Mock()
|
||||
entry.data = ENTRY_CONFIG
|
||||
|
||||
axis_device = device.AxisNetworkDevice(hass, entry)
|
||||
axis_device.api = Mock()
|
||||
|
||||
axis_device.shutdown(None)
|
||||
|
||||
assert len(axis_device.api.stop.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_get_device(hass):
|
||||
"""Successful call."""
|
||||
with patch('axis.vapix.Vapix.load_params',
|
||||
return_value=mock_coro()):
|
||||
assert await device.get_device(hass, DEVICE_DATA)
|
||||
|
||||
|
||||
async def test_get_device_fails(hass):
|
||||
"""Device unauthorized yields authentication required error."""
|
||||
import axis
|
||||
|
||||
with patch('axis.vapix.Vapix.load_params',
|
||||
side_effect=axis.Unauthorized), \
|
||||
pytest.raises(errors.AuthenticationRequired):
|
||||
await device.get_device(hass, DEVICE_DATA)
|
||||
|
||||
|
||||
async def test_get_device_device_unavailable(hass):
|
||||
"""Device unavailable yields cannot connect error."""
|
||||
import axis
|
||||
|
||||
with patch('axis.vapix.Vapix.load_params',
|
||||
side_effect=axis.RequestError), \
|
||||
pytest.raises(errors.CannotConnect):
|
||||
await device.get_device(hass, DEVICE_DATA)
|
||||
|
||||
|
||||
async def test_get_device_unknown_error(hass):
|
||||
"""Device yield unknown error."""
|
||||
import axis
|
||||
|
||||
with patch('axis.vapix.Vapix.load_params',
|
||||
side_effect=axis.AxisException), \
|
||||
pytest.raises(errors.AuthenticationRequired):
|
||||
await device.get_device(hass, DEVICE_DATA)
|
|
@ -0,0 +1,97 @@
|
|||
"""Test Axis component setup process."""
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.components import axis
|
||||
|
||||
from tests.common import mock_coro, MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup(hass):
|
||||
"""Test configured options for a device are loaded via config entry."""
|
||||
with patch.object(hass, 'config_entries') as mock_config_entries, \
|
||||
patch.object(axis, 'configured_devices', return_value={}):
|
||||
|
||||
assert await async_setup_component(hass, axis.DOMAIN, {
|
||||
axis.DOMAIN: {
|
||||
'device_name': {
|
||||
axis.CONF_HOST: '1.2.3.4',
|
||||
axis.config_flow.CONF_PORT: 80,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
assert len(mock_config_entries.flow.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_setup_device_already_configured(hass):
|
||||
"""Test already configured device does not configure a second."""
|
||||
with patch.object(hass, 'config_entries') as mock_config_entries, \
|
||||
patch.object(axis, 'configured_devices', return_value={'1.2.3.4'}):
|
||||
|
||||
assert await async_setup_component(hass, axis.DOMAIN, {
|
||||
axis.DOMAIN: {
|
||||
'device_name': {
|
||||
axis.CONF_HOST: '1.2.3.4'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
assert not mock_config_entries.flow.mock_calls
|
||||
|
||||
|
||||
async def test_setup_no_config(hass):
|
||||
"""Test setup without configuration."""
|
||||
assert await async_setup_component(hass, axis.DOMAIN, {})
|
||||
assert axis.DOMAIN not in hass.data
|
||||
|
||||
|
||||
async def test_setup_entry(hass):
|
||||
"""Test successful setup of entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=axis.DOMAIN, data={axis.device.CONF_MAC: '0123'})
|
||||
|
||||
mock_device = Mock()
|
||||
mock_device.async_setup.return_value = mock_coro(True)
|
||||
mock_device.serial.return_value = '1'
|
||||
|
||||
with patch.object(axis, 'AxisNetworkDevice') as mock_device_class, \
|
||||
patch.object(
|
||||
axis, 'async_populate_options', return_value=mock_coro(True)):
|
||||
mock_device_class.return_value = mock_device
|
||||
|
||||
assert await axis.async_setup_entry(hass, entry)
|
||||
|
||||
assert len(hass.data[axis.DOMAIN]) == 1
|
||||
|
||||
|
||||
async def test_setup_entry_fails(hass):
|
||||
"""Test successful setup of entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=axis.DOMAIN, data={axis.device.CONF_MAC: '0123'}, options=True)
|
||||
|
||||
mock_device = Mock()
|
||||
mock_device.async_setup.return_value = mock_coro(False)
|
||||
|
||||
with patch.object(axis, 'AxisNetworkDevice') as mock_device_class:
|
||||
mock_device_class.return_value = mock_device
|
||||
|
||||
assert not await axis.async_setup_entry(hass, entry)
|
||||
|
||||
assert not hass.data[axis.DOMAIN]
|
||||
|
||||
|
||||
async def test_populate_options(hass):
|
||||
"""Test successful populate options."""
|
||||
entry = MockConfigEntry(domain=axis.DOMAIN, data={'device': {}})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch.object(axis, 'get_device', return_value=mock_coro(Mock())):
|
||||
|
||||
await axis.async_populate_options(hass, entry)
|
||||
|
||||
assert entry.options == {
|
||||
axis.CONF_CAMERA: True,
|
||||
axis.CONF_EVENTS: True,
|
||||
axis.CONF_TRIGGER_TIME: axis.DEFAULT_TRIGGER_TIME
|
||||
}
|
Loading…
Reference in New Issue