Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ omit =
homeassistant/components/camera/ring.py
homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/synology.py
homeassistant/components/camera/xeoma.py
homeassistant/components/camera/yi.py
homeassistant/components/climate/econet.py
homeassistant/components/climate/ephember.py
Expand Down
112 changes: 112 additions & 0 deletions homeassistant/components/camera/xeoma.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""
Support for Xeoma Cameras.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.xeoma/
"""
import asyncio
import logging

import voluptuous as vol

from homeassistant.components.camera import (
PLATFORM_SCHEMA, Camera)
from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_HOST)
from homeassistant.helpers import config_validation as cv

_LOGGER = logging.getLogger(__name__)

REQUIREMENTS = ['pyxeoma==1.2']

CONF_CAMERAS = 'cameras'
CONF_HIDE = 'hide'
CONF_IMAGE_NAME = 'image_name'
CONF_NEW_VERSION = 'new_version'

CAMERAS_SCHEMA = vol.Schema({
vol.Required(CONF_IMAGE_NAME): cv.string,
vol.Optional(CONF_HIDE, default=False): cv.boolean,
vol.Optional(CONF_NAME): cv.string
}, required=False)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_CAMERAS, default={}):
vol.Schema(vol.All(cv.ensure_list, [CAMERAS_SCHEMA])),
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_NEW_VERSION, default=True): cv.boolean
})


@asyncio.coroutine
# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Discover and setup Xeoma Cameras."""
host = config[CONF_HOST]
login = config[CONF_USERNAME] if CONF_USERNAME in config else None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use login = config.get(CONF_USERNAME). This way it will be None if no username was set in the configuration.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Done

password = config[CONF_PASSWORD] if CONF_PASSWORD in config else None
new_version = config[CONF_NEW_VERSION]

from pyxeoma.xeoma import Xeoma, XeomaError
xeoma = Xeoma(host, new_version, login, password)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that there will be a trackback if the credentials are wrong or the camera is not reachable. Please catch that, log an error that the users knows what's going on and return. Otherwise the users end up with a non-functional platform.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Xeoma is an NVR similar to BlueIris or Zoneminder so the connection being made is not to individual cameras, but to a web interface that can be enabled within Xeoma which provides direct access to jpgs from all connected cameras. The constructor for the Xeoma object doesn't make any attempt to connect to the Xeoma web server. All of the calls to the library that can fail are inside the following try block. The first function call inside the try block, xeoma.async_test_connection(), will test the connection to the server. If this fails, the library raises a XeomaError with an appropriate message and the platform logs that.

As far as authentication, there are 2 possible failure scenarios that I can see:

  1. Incorrect credentials are provided: The library will attempt to obtain a session key, but fail and it will raise a XeomaError.
  2. No credential are provided, but credentials are required: The library will attempt to discover cameras, but none will be found and it will raise a XeomaError.

In both cases the platform will catch and log the error and since it will only add entities to home assistant for cameras it is able to discover, no non-working camera entities will be added.

The main issue is that the web server that Xeoma provides doesn't handle HTTP status codes correctly. In scenario 2 above, you still get a 200 response which makes it difficult to determine exactly why camera discovery failed since incorrect configuration of the web server module within Xeoma can also create a situation in which no cameras will be discovered (though this should be obvious since the user won't be able to access their cameras in the Xeoma web ui either). The error message provided in this scenario simply informs the user that no cameras were discovered.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pyxeoma is handling all connection issues. So it makes sense to just push then through.


try:
yield from xeoma.async_test_connection()
discovered_image_names = yield from xeoma.async_get_image_names()
discovered_cameras = [
{
CONF_IMAGE_NAME: image_name,
CONF_HIDE: False,
CONF_NAME: image_name
}
for image_name in discovered_image_names
]

for cam in config[CONF_CAMERAS]:
camera = next(
(dc for dc in discovered_cameras
if dc[CONF_IMAGE_NAME] == cam[CONF_IMAGE_NAME]), None)

if camera is not None:
if CONF_NAME in cam:
camera[CONF_NAME] = cam[CONF_NAME]
if CONF_HIDE in cam:
camera[CONF_HIDE] = cam[CONF_HIDE]

cameras = list(filter(lambda c: not c[CONF_HIDE], discovered_cameras))
async_add_devices(
[XeomaCamera(xeoma, camera[CONF_IMAGE_NAME], camera[CONF_NAME])
for camera in cameras])
except XeomaError as err:
_LOGGER.error('XeomaError: %s', err.message)


class XeomaCamera(Camera):
"""Implementation of a Xeoma camera."""

def __init__(self, xeoma, image, name):
"""Initialize a Xeoma camera."""
super().__init__()
self._xeoma = xeoma
self._name = name
self._image = image
self._last_image = None

@asyncio.coroutine
def async_camera_image(self):
"""Return a still image response from the camera."""
from pyxeoma.xeoma import XeomaError
try:
image = yield from self._xeoma.async_get_camera_image(self._image)
self._last_image = image
except XeomaError as err:
_LOGGER.error("Error fetching image from Xeoma: %s", err.message)

return self._last_image

@property
def name(self):
"""Return the name of this device."""
return self._name
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,9 @@ pywebpush==1.5.0
# homeassistant.components.wemo
pywemo==0.4.25

# homeassistant.components.camera.xeoma
pyxeoma==1.2

# homeassistant.components.zabbix
pyzabbix==0.7.4

Expand Down