Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Axis config flow #18543

Merged
merged 22 commits into from
Mar 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions homeassistant/components/axis/.translations/en.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
268 changes: 41 additions & 227 deletions homeassistant/components/axis/__init__.py
Original file line number Diff line number Diff line change
@@ -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] = {}
if DOMAIN in config:

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()
for device_name, device_config in config[DOMAIN].items():

hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
if CONF_NAME not in device_config:
device_config[CONF_NAME] = device_name

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 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
))

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)
return True

# 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]
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])
async def async_setup_entry(hass, config_entry):
"""Set up the Axis component."""
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}

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 not config_entry.options:
await async_populate_options(hass, config_entry)

# Register service with Home Assistant.
hass.services.register(
DOMAIN, SERVICE_VAPIX_CALL, vapix_service, schema=SERVICE_SCHEMA)
return True
device = AxisNetworkDevice(hass, config_entry)

if not await device.async_setup():
return False

def setup_device(hass, config, device_config):
"""Set up an Axis device."""
import axis
hass.data[DOMAIN][device.serial] = device

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)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown)

event_types = [
event
for event in device_config[CONF_INCLUDE]
if event in EVENT_TYPES
]
return True

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
async def async_populate_options(hass, config_entry):
"""Populate default options for device."""
from axis.vapix import VAPIX_IMAGE_FORMAT

except axis.Unauthorized:
_LOGGER.error("Credentials for %s are faulty",
device_config[CONF_HOST])
return False
device = await get_device(hass, config_entry.data[CONF_DEVICE])

except axis.RequestError:
return False
supported_formats = device.vapix.get_param(VAPIX_IMAGE_FORMAT)

device.name = device_config[CONF_NAME]
camera = bool(supported_formats)

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)
options = {
CONF_CAMERA: camera,
CONF_EVENTS: True,
CONF_TRIGGER_TIME: DEFAULT_TRIGGER_TIME
}

if event_types:
hass.add_job(device.start)
return True
hass.config_entries.async_update_entry(config_entry, options=options)
Loading