init
This commit is contained in:
73
custom_components/camera/shinobi.py
Normal file
73
custom_components/camera/shinobi.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import homeassistant.loader as loader
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['shinobi']
|
||||
DOMAIN = 'shinobi'
|
||||
|
||||
shinobi = loader.get_component('shinobi')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
monitors = shinobi.get_all_started_monitors()
|
||||
cameras = []
|
||||
|
||||
_LOGGER.debug(config)
|
||||
|
||||
if not monitors:
|
||||
_LOGGER.warning('No active monitors found')
|
||||
return
|
||||
|
||||
for monitor in monitors:
|
||||
device_info = {
|
||||
CONF_NAME: monitor['name'],
|
||||
CONF_MJPEG_URL: shinobi.monitor_stream_url(monitor['mid']),
|
||||
CONF_STILL_IMAGE_URL: shinobi.monitor_still_url(monitor['mid'])
|
||||
}
|
||||
cameras.append(ShinobiCamera(hass, device_info, monitor))
|
||||
|
||||
if not cameras:
|
||||
_LOGGER.warning('No active cameras found')
|
||||
return
|
||||
|
||||
async_add_devices(cameras)
|
||||
|
||||
|
||||
class ShinobiCamera(MjpegCamera):
|
||||
"""Representation of a Shinobi Monitor Stream."""
|
||||
|
||||
def __init__(self, hass, device_info, monitor):
|
||||
"""Initialize as a subclass of MjpegCamera."""
|
||||
super().__init__(hass, device_info)
|
||||
self._monitor_id = monitor['mid']
|
||||
self._is_recording = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Update the recording state periodically."""
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
"""Update our recording state from the Shinobi API."""
|
||||
_LOGGER.debug('Updating camera state for monitor {}'.format(self._monitor_id))
|
||||
|
||||
status_response = shinobi.get_monitor_state(self._monitor_id)
|
||||
|
||||
if not status_response:
|
||||
_LOGGER.warning('Could not get status for monitor {}'.format(self._monitor_id))
|
||||
return
|
||||
_LOGGER.debug('Monitor {} is in status {}'.format(self._monitor_id, status_response['mode']))
|
||||
self._is_recording = status_response.get('status') == shinobi.SHINOBI_CAM_RECORDING
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return whether the monitor is in recording mode."""
|
||||
return self._is_recording
|
||||
|
||||
125
custom_components/shinobi.py
Normal file
125
custom_components/shinobi.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import requests
|
||||
import json
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_API_KEY, CONF_SSL)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'shinobi'
|
||||
DEFAULT_TIMEOUT = 10
|
||||
DEFAULT_SSL = False
|
||||
|
||||
SHINOBI_CAM_DISABLED = 'stop'
|
||||
SHINOBI_CAM_WATCHING = 'start'
|
||||
SHINOBI_CAM_RECORDING = 'record'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required('group_key'): cv.string,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
vol.Optional('monitors', default=[]): cv.ensure_list
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
|
||||
global SHINOBI
|
||||
SHINOBI = {}
|
||||
|
||||
conf = config[DOMAIN]
|
||||
if conf[CONF_SSL]:
|
||||
schema = 'https'
|
||||
else:
|
||||
schema = 'http'
|
||||
|
||||
server_origin = '{}://{}'.format(schema, conf[CONF_HOST])
|
||||
|
||||
SHINOBI['server_origin'] = server_origin
|
||||
SHINOBI['api_key'] = conf.get(CONF_API_KEY, None)
|
||||
SHINOBI['group_key'] = conf.get('group_key', None)
|
||||
SHINOBI['monitors'] = conf.get('monitors')
|
||||
|
||||
hass.data[DOMAIN] = SHINOBI
|
||||
|
||||
# unfortunately, the api does not return error codes. The only way to check if the credentials are working is to check the response of an (arbitrary) request (e.g. get all monitors)
|
||||
try:
|
||||
check_creds_response = get_all_started_monitors()
|
||||
except:
|
||||
return False
|
||||
|
||||
# expected response contains a list with activated monitors (or an empty list)
|
||||
if isinstance(check_creds_response, list):
|
||||
return True
|
||||
else:
|
||||
# response payload contains ok: false if authentication has not been successful
|
||||
if not check_creds_response.ok:
|
||||
_LOGGER.error('Wrong api_key or non existing group_key provided')
|
||||
else:
|
||||
_LOGGER.error('Unknown error occured while retrieving monitors')
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _shinobi_request(api_path, method='get', data=None):
|
||||
"""Perform a Shinobi request."""
|
||||
|
||||
api_base = SHINOBI['server_origin'] + '/' + SHINOBI['api_key']
|
||||
|
||||
req = requests.get(api_base + api_path, timeout=DEFAULT_TIMEOUT)
|
||||
|
||||
# TODO some more error handling here?
|
||||
try:
|
||||
response = req.json()
|
||||
except ValueError:
|
||||
_LOGGER.exception('JSON decode exception caught while attempting to '
|
||||
'decode "{}"', req.text)
|
||||
return response
|
||||
|
||||
|
||||
def get_all_started_monitors():
|
||||
"""Get all started monitors from the Shinobi API."""
|
||||
_LOGGER.debug('Sending request to Shinobi to get all started monitors')
|
||||
|
||||
get_monitors_path = '/smonitor/' + SHINOBI['group_key']
|
||||
|
||||
response = _shinobi_request(get_monitors_path)
|
||||
|
||||
# TODO is it necessary to save monitors globally in SHINOBI?
|
||||
SHINOBI['monitors'] = response
|
||||
|
||||
_LOGGER.debug('Shinobi returned {} monitors: {}'.format(str(len(SHINOBI['monitors'])), str([monitor['name'] for monitor in SHINOBI['monitors']])))
|
||||
|
||||
return SHINOBI['monitors']
|
||||
|
||||
|
||||
def get_monitor_state(monitor_id):
|
||||
"""Get the state of a monitor."""
|
||||
api_path = '/monitor/{}/{}'.format(SHINOBI['group_key'], monitor_id)
|
||||
return _shinobi_request(api_path)
|
||||
|
||||
|
||||
def set_monitor_state(monitor_id, mode):
|
||||
"""Get the state of a monitor."""
|
||||
if not (mode == SHINOBI_CAM_DISABLED or mode == SHINOBI_CAM_WATCHING or mode == SHINOBI_CAM_RECORDING):
|
||||
raise ValueError('monitor state must be either {}, {} or {}'.format(SHINOBI_CAM_DISABLED, SHINOBI_CAM_WATCHING, SHINOBI_CAM_RECORDING))
|
||||
api_path = '/monitor/{}/{}'.format(SHINOBI['group_key'], monitor_id)
|
||||
return _shinobi_request(api_path)
|
||||
|
||||
|
||||
def monitor_stream_url(monitor_id):
|
||||
"""Get the stream url. See https://shinobi.video/docs/api#content-embedding-streams for more information."""
|
||||
return SHINOBI['server_origin'] + '/' + SHINOBI['api_key'] + '/embed/' + SHINOBI['group_key'] + '/' + monitor_id
|
||||
|
||||
|
||||
def monitor_still_url(monitor_id):
|
||||
"""Get the url of still jpg images. Snapshots must be enabled in cam settings, see https://shinobi.video/docs/api#content-get-streams"""
|
||||
return SHINOBI['server_origin'] + '/' + SHINOBI['api_key'] + '/jpeg/' + SHINOBI['group_key'] + '/' + monitor_id + '/s.jpg'
|
||||
Reference in New Issue
Block a user