}
if settings.LOCAL_TIME_FILE:
- format_dict['timezone'] = tzctl.get_time_zone()
+ format_dict['timezone'] = tzctl._get_time_zone()
else:
format_dict['timezone'] = 'local time'
import re
import shlex
-from collections import OrderedDict
-
import diskctl
import motionctl
import settings
import smbctl
-import tzctl
import update
import utils
import v4l2ctl
-import wifictl
+
+from utils import OrderedDict
_CAMERA_CONFIG_FILE_NAME = 'thread-%(id)s.conf'
_main_config_cache = None
_camera_config_cache = {}
_camera_ids_cache = None
+_additional_section_funcs = []
+_additional_config_funcs = []
+_additional_structure_cache = {}
# starting with r490 motion config directives have changed a bit
_LAST_OLD_CONFIG_VERSIONS = (490, '3.2.12')
+def additional_section(func):
+ _additional_section_funcs.append(func)
+
+
+def additional_config(func):
+ _additional_config_funcs.append(func)
+
+
+import wifictl # @UnusedImport
+import tzctl # @UnusedImport
+
+
def get_main(as_lines=False):
global _main_config_cache
list_names=['thread'],
no_convert=['@admin_username', '@admin_password', '@normal_username', '@normal_password'])
- if settings.WPA_SUPPLICANT_CONF:
- _get_wifi_settings(main_config)
-
- if settings.LOCAL_TIME_FILE:
- _get_localtime_settings(main_config)
-
+ _get_additional_config(main_config, camera=False)
_set_default_motion(main_config, old_motion=_is_old_motion())
_main_config_cache = main_config
_main_config_cache = main_config
main_config = dict(main_config)
- _set_wifi_settings(main_config)
- _set_localtime_settings(main_config)
-
+ _set_additional_config(main_config, camera=False)
+
config_file_path = os.path.join(settings.CONF_PATH, _MAIN_CONFIG_FILE_NAME)
# read the actual configuration from file
def main_ui_to_dict(ui):
- return {
+ data = {
'@enabled': ui['enabled'],
'@show_advanced': ui['show_advanced'],
'@admin_username': ui['admin_username'],
'@admin_password': ui['admin_password'],
'@normal_username': ui['normal_username'],
- '@normal_password': ui['normal_password'],
- '@time_zone': ui['time_zone'],
-
- '@wifi_enabled': ui['wifi_enabled'],
- '@wifi_name': ui['wifi_name'],
- '@wifi_key': ui['wifi_key'],
+ '@normal_password': ui['normal_password']
}
+ # additional configs
+ for name, value in ui.iteritems():
+ if not name.startswith('_'):
+ continue
+
+ data['@' + name] = value
+
+ return data
+
def main_dict_to_ui(data):
- return {
+ ui = {
'enabled': data['@enabled'],
'show_advanced': data['@show_advanced'],
'admin_username': data['@admin_username'],
'admin_password': data['@admin_password'],
'normal_username': data['@normal_username'],
- 'normal_password': data['@normal_password'],
- 'time_zone': data['@time_zone'],
-
- 'wifi_enabled': data['@wifi_enabled'],
- 'wifi_name': data['@wifi_name'],
- 'wifi_key': data['@wifi_key'],
+ 'normal_password': data['@normal_password']
}
+ # additional configs
+ for name, value in data.iteritems():
+ if not name.startswith('@_'):
+ continue
+
+ ui[name[1:]] = value
+
+ return ui
+
def camera_ui_to_dict(ui):
data = {
on_event_end = ['%(script)s stop %%t' % {'script': event_relay_path}]
data['on_event_end'] = '; '.join(on_event_end)
+
+ # additional configs
+ for name, value in ui.iteritems():
+ if not name.startswith('_'):
+ continue
+
+ data['@' + name] = value
return data
ui['command_notifications_enabled'] = True
ui['command_notifications_exec'] = '; '.join(command_notifications)
+ # additional configs
+ for name, value in data.iteritems():
+ if not name.startswith('@_'):
+ continue
+
+ ui[name[1:]] = value
+
return ui
conf_lines.append('') # add a blank line
for (name, value) in remaining.iteritems():
+ if name.startswith('@_'):
+ continue # ignore additional configs
+
if name in list_names:
for v in value:
line = name + ' ' + _python_to_value(v)
data.setdefault('@admin_password', '')
data.setdefault('@normal_username', 'user')
data.setdefault('@normal_password', '')
- data.setdefault('@time_zone', 'UTC')
- data.setdefault('@wifi_enabled', False)
- data.setdefault('@wifi_name', '')
- data.setdefault('@wifi_key', '')
-
if old_motion:
data.setdefault('control_port', 7999)
data.setdefault('on_event_end', '')
-def _get_wifi_settings(data):
- wifi_settings = wifictl.get_wifi_settings()
+def get_additional_structure(camera):
+ if _additional_structure_cache.get(camera) is None:
+ logging.debug('loading additional config structure for %s' % ('camera' if camera else 'main'))
- data['@wifi_enabled'] = bool(wifi_settings['ssid'])
- data['@wifi_name'] = wifi_settings['ssid']
- data['@wifi_key'] = wifi_settings['psk']
+ # gather sections
+ sections = OrderedDict()
+ for func in _additional_section_funcs:
+ result = func()
+ if not result:
+ continue
+
+ if result.get('reboot') and not settings.ENABLE_REBOOT:
+ continue
+
+ if bool(result.get('camera')) != bool(camera):
+ continue
-
-def _set_wifi_settings(data):
- wifi_enabled = data.pop('@wifi_enabled', False)
- wifi_name = data.pop('@wifi_name', '')
- wifi_key = data.pop('@wifi_key', '')
+ result['name'] = func.func_name
+ sections[func.func_name] = result
- if settings.WPA_SUPPLICANT_CONF:
- s = {
- 'ssid': wifi_enabled and wifi_name,
- 'psk': wifi_key
- }
-
- wifictl.set_wifi_settings(s)
+ configs = OrderedDict()
+ for func in _additional_config_funcs:
+ result = func()
+ if not result:
+ continue
+
+ if result.get('reboot') and not settings.ENABLE_REBOOT:
+ continue
+
+ if bool(result.get('camera')) != bool(camera):
+ continue
+
+ result['name'] = func.func_name
+ configs[func.func_name] = result
+
+ section = sections.setdefault(result.get('section'), {})
+ section.setdefault('configs', []).append(result)
+
+ _additional_structure_cache[camera] = sections, configs
+
+ return _additional_structure_cache[camera]
-def _get_localtime_settings(data):
- time_zone = tzctl.get_time_zone()
- data['@time_zone'] = time_zone
+def _get_additional_config(data, camera):
+ (sections, configs) = get_additional_structure(camera)
+ get_funcs = set([c.get('get') for c in configs.itervalues() if c.get('get')])
+ get_func_values = dict((f, f()) for f in get_funcs)
+ for name, section in sections.iteritems():
+ if not section.get('get'):
+ continue
+
+ if section.get('get_set_dict'):
+ data['@_' + name] = get_func_values.get(section['get'], {}).get(name)
+
+ else:
+ data['@_' + name] = get_func_values.get(section['get'])
+
+ for name, config in configs.iteritems():
+ if not config.get('get'):
+ continue
+
+ if config.get('get_set_dict'):
+ data['@_' + name] = get_func_values.get(config['get'], {}).get(name)
+
+ else:
+ data['@_' + name] = get_func_values.get(config['get'])
+
+
+def _set_additional_config(data, camera):
+ (sections, configs) = get_additional_structure(camera)
+
+ set_func_values = {}
+ for name, section in sections.iteritems():
+ if not section.get('set'):
+ continue
+
+ if section.get('get_set_dict'):
+ set_func_values.setdefault(section['set'], {})[name] = data.get('@_' + name)
+
+ else:
+ set_func_values[section['set']] = data.get('@_' + name)
+
+ for name, config in configs.iteritems():
+ if not config.get('set'):
+ continue
+
+ if config.get('get_set_dict'):
+ set_func_values.setdefault(config['set'], {})[name] = data.get('@_' + name)
+
+ else:
+ set_func_values[config['set']] = data.get('@_' + name)
-def _set_localtime_settings(data):
- time_zone = data.pop('@time_zone')
- if time_zone and settings.LOCAL_TIME_FILE:
- tzctl.set_time_zone(time_zone)
+ for func, value in set_func_values.iteritems():
+ func(value)
def get(self):
import motioneye
- timezones = []
- if settings.LOCAL_TIME_FILE:
- import pytz
- timezones = pytz.common_timezones
+ # additional config
+ main_sections = config.get_additional_structure(camera=False)[0]
+ camera_sections = config.get_additional_structure(camera=True)[0]
self.render('main.html',
frame=False,
version=motioneye.VERSION,
enable_update=bool(settings.REPO),
- wpa_supplicant=settings.WPA_SUPPLICANT_CONF,
enable_reboot=settings.ENABLE_REBOOT,
- timezones=timezones,
+ main_sections=main_sections,
+ camera_sections=camera_sections,
hostname=socket.gethostname(),
admin_username=config.get_main().get('@admin_username'))
main_config = config.main_ui_to_dict(ui_config)
main_config.setdefault('thread', old_main_config.get('thread', []))
admin_credentials = '%s:%s' % (main_config.get('@admin_username', ''), main_config.get('@admin_password', ''))
-
- wifi_changed = bool([k for k in ['@wifi_enabled', '@wifi_name', '@wifi_key'] if old_main_config.get(k) != main_config.get(k)])
-
+
+ additional_configs = config.get_additional_structure(camera=False)[1]
+ reboot_config_names = [('@_' + c['name']) for c in additional_configs.values() if c.get('reboot')]
+ reboot = bool([k for k in reboot_config_names if old_main_config.get(k) != main_config.get(k)])
+
config.set_main(main_config)
- reboot = False
reload = False
if admin_credentials != old_admin_credentials:
reload = True
- if wifi_changed:
- logging.debug('wifi settings changed, reboot needed')
+ if reboot:
+ logging.debug('system settings changed, reboot needed')
reboot = True
-
+
return {'reload': reload, 'reboot': reboot}
reload = False # indicates that browser should reload the page
if reboot[0]:
if settings.ENABLE_REBOOT:
def call_reboot():
- logging.info('rebooting')
- os.system('reboot')
+ powerctl.reboot()
ioloop = IOLoop.instance()
ioloop.add_timeout(datetime.timedelta(seconds=2), call_reboot)
event = self.get_argument('event')
logging.debug('event %(event)s relayed for camera with id %(id)s' % {'event': event, 'id': camera_id})
- camera_config = config.get_camera(camera_id)
+ try:
+ camera_config = config.get_camera(camera_id)
+
+ except:
+ logging.warn('ignoring event for remote camera with id %s (probably removed)' % camera_id)
+ return self.finish_json()
+
if not utils.local_camera(camera_config):
logging.warn('ignoring event for remote camera with id %s' % camera_id)
return self.finish_json()
--- /dev/null
+# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
+# Passes Python2.7's test suite and incorporates all the latest updates.
+
+try:
+ from thread import get_ident as _get_ident
+except ImportError:
+ from dummy_thread import get_ident as _get_ident
+
+try:
+ from _abcoll import KeysView, ValuesView, ItemsView
+except ImportError:
+ pass
+
+
+class OrderedDict(dict):
+ 'Dictionary that remembers insertion order'
+ # An inherited dict maps keys to values.
+ # The inherited dict provides __getitem__, __len__, __contains__, and get.
+ # The remaining methods are order-aware.
+ # Big-O running times for all methods are the same as for regular dictionaries.
+
+ # The internal self.__map dictionary maps keys to links in a doubly linked list.
+ # The circular doubly linked list starts and ends with a sentinel element.
+ # The sentinel element never gets deleted (this simplifies the algorithm).
+ # Each link is stored as a list of length three: [PREV, NEXT, KEY].
+
+ def __init__(self, *args, **kwds):
+ '''Initialize an ordered dictionary. Signature is the same as for
+ regular dictionaries, but keyword arguments are not recommended
+ because their insertion order is arbitrary.
+
+ '''
+ if len(args) > 1:
+ raise TypeError('expected at most 1 arguments, got %d' % len(args))
+ try:
+ self.__root
+ except AttributeError:
+ self.__root = root = [] # sentinel node
+ root[:] = [root, root, None]
+ self.__map = {}
+ self.__update(*args, **kwds)
+
+ def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
+ 'od.__setitem__(i, y) <==> od[i]=y'
+ # Setting a new item creates a new link which goes at the end of the linked
+ # list, and the inherited dictionary is updated with the new key/value pair.
+ if key not in self:
+ root = self.__root
+ last = root[0]
+ last[1] = root[0] = self.__map[key] = [last, root, key]
+ dict_setitem(self, key, value)
+
+ def __delitem__(self, key, dict_delitem=dict.__delitem__):
+ 'od.__delitem__(y) <==> del od[y]'
+ # Deleting an existing item uses self.__map to find the link which is
+ # then removed by updating the links in the predecessor and successor nodes.
+ dict_delitem(self, key)
+ link_prev, link_next, key = self.__map.pop(key)
+ link_prev[1] = link_next
+ link_next[0] = link_prev
+
+ def __iter__(self):
+ 'od.__iter__() <==> iter(od)'
+ root = self.__root
+ curr = root[1]
+ while curr is not root:
+ yield curr[2]
+ curr = curr[1]
+
+ def __reversed__(self):
+ 'od.__reversed__() <==> reversed(od)'
+ root = self.__root
+ curr = root[0]
+ while curr is not root:
+ yield curr[2]
+ curr = curr[0]
+
+ def clear(self):
+ 'od.clear() -> None. Remove all items from od.'
+ try:
+ for node in self.__map.itervalues():
+ del node[:]
+ root = self.__root
+ root[:] = [root, root, None]
+ self.__map.clear()
+ except AttributeError:
+ pass
+ dict.clear(self)
+
+ def popitem(self, last=True):
+ '''od.popitem() -> (k, v), return and remove a (key, value) pair.
+ Pairs are returned in LIFO order if last is true or FIFO order if false.
+
+ '''
+ if not self:
+ raise KeyError('dictionary is empty')
+ root = self.__root
+ if last:
+ link = root[0]
+ link_prev = link[0]
+ link_prev[1] = root
+ root[0] = link_prev
+ else:
+ link = root[1]
+ link_next = link[1]
+ root[1] = link_next
+ link_next[0] = root
+ key = link[2]
+ del self.__map[key]
+ value = dict.pop(self, key)
+ return key, value
+
+ # -- the following methods do not depend on the internal structure --
+
+ def keys(self):
+ 'od.keys() -> list of keys in od'
+ return list(self)
+
+ def values(self):
+ 'od.values() -> list of values in od'
+ return [self[key] for key in self]
+
+ def items(self):
+ 'od.items() -> list of (key, value) pairs in od'
+ return [(key, self[key]) for key in self]
+
+ def iterkeys(self):
+ 'od.iterkeys() -> an iterator over the keys in od'
+ return iter(self)
+
+ def itervalues(self):
+ 'od.itervalues -> an iterator over the values in od'
+ for k in self:
+ yield self[k]
+
+ def iteritems(self):
+ 'od.iteritems -> an iterator over the (key, value) items in od'
+ for k in self:
+ yield (k, self[k])
+
+ def update(*args, **kwds): #@NoSelf
+ '''od.update(E, **F) -> None. Update od from dict/iterable E and F.
+
+ If E is a dict instance, does: for k in E: od[k] = E[k]
+ If E has a .keys() method, does: for k in E.keys(): od[k] = E[k]
+ Or if E is an iterable of items, does: for k, v in E: od[k] = v
+ In either case, this is followed by: for k, v in F.items(): od[k] = v
+
+ '''
+ if len(args) > 2:
+ raise TypeError('update() takes at most 2 positional '
+ 'arguments (%d given)' % (len(args),))
+ elif not args:
+ raise TypeError('update() takes at least 1 argument (0 given)')
+ self = args[0]
+ # Make progressively weaker assumptions about "other"
+ other = ()
+ if len(args) == 2:
+ other = args[1]
+ if isinstance(other, dict):
+ for key in other:
+ self[key] = other[key]
+ elif hasattr(other, 'keys'):
+ for key in other.keys():
+ self[key] = other[key]
+ else:
+ for key, value in other:
+ self[key] = value
+ for key, value in kwds.items():
+ self[key] = value
+
+ __update = update # let subclasses override update without breaking __init__
+
+ __marker = object()
+
+ def pop(self, key, default=__marker):
+ '''od.pop(k[,d]) -> v, remove specified key and return the corresponding value.
+ If key is not found, d is returned if given, otherwise KeyError is raised.
+
+ '''
+ if key in self:
+ result = self[key]
+ del self[key]
+ return result
+ if default is self.__marker:
+ raise KeyError(key)
+ return default
+
+ def setdefault(self, key, default=None):
+ 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od'
+ if key in self:
+ return self[key]
+ self[key] = default
+ return default
+
+ def __repr__(self, _repr_running={}):
+ 'od.__repr__() <==> repr(od)'
+ call_key = id(self), _get_ident()
+ if call_key in _repr_running:
+ return '...'
+ _repr_running[call_key] = 1
+ try:
+ if not self:
+ return '%s()' % (self.__class__.__name__,)
+ return '%s(%r)' % (self.__class__.__name__, self.items())
+ finally:
+ del _repr_running[call_key]
+
+ def __reduce__(self):
+ 'Return state information for pickling'
+ items = [[k, self[k]] for k in self]
+ inst_dict = vars(self).copy()
+ for k in vars(OrderedDict()):
+ inst_dict.pop(k, None)
+ if inst_dict:
+ return (self.__class__, (items,), inst_dict)
+ return self.__class__, (items,)
+
+ def copy(self):
+ 'od.copy() -> a shallow copy of od'
+ return self.__class__(self)
+
+ @classmethod
+ def fromkeys(cls, iterable, value=None):
+ '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S
+ and values equal to v (which defaults to None).
+
+ '''
+ d = cls()
+ for key in iterable:
+ d[key] = value
+ return d
+
+ def __eq__(self, other):
+ '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive
+ while comparison to a regular mapping is order-insensitive.
+
+ '''
+ if isinstance(other, OrderedDict):
+ return len(self)==len(other) and self.items() == other.items()
+ return dict.__eq__(self, other)
+
+ def __ne__(self, other):
+ return not self == other
+
+ # -- the following methods are only used in Python 2.7 --
+
+ def viewkeys(self):
+ "od.viewkeys() -> a set-like object providing a view on od's keys"
+ return KeysView(self)
+
+ def viewvalues(self):
+ "od.viewvalues() -> an object providing a view on od's values"
+ return ValuesView(self)
+
+ def viewitems(self):
+ "od.viewitems() -> a set-like object providing a view on od's items"
+ return ItemsView(self)
import settings
import subprocess
+from config import additional_config
+
+
+LOCAL_TIME_FILE = settings.LOCAL_TIME_FILE # @UndefinedVariable
+
def _get_time_zone_symlink():
file = settings.LOCAL_TIME_FILE
return time_zone
-def get_time_zone():
+def _get_time_zone():
time_zone = _get_time_zone_symlink() or _get_time_zone_md5()
if not time_zone:
logging.error('could not find local time zone')
return time_zone
-def set_time_zone(time_zone):
+def _set_time_zone(time_zone):
zoneinfo_file = '/usr/share/zoneinfo/' + time_zone
if not os.path.exists(zoneinfo_file):
logging.error('%s file does not exist' % zoneinfo_file)
logging.error('failed to link "%s" to "%s": %s' % (settings.LOCAL_TIME_FILE, zoneinfo_file, e))
return False
+
+
+@additional_config
+def timeZone():
+ if not LOCAL_TIME_FILE:
+ return
+
+ import pytz
+ timezones = pytz.common_timezones
+
+ return {
+ 'label': 'Time Zone',
+ 'description': 'selecting the right timezone assures a correct timestamp displayed on pictures and movies',
+ 'type': 'choices',
+ 'choices': [(t, t) for t in timezones],
+ 'section': 'general',
+ 'advanced': True,
+ 'get': _get_time_zone,
+ 'set': _set_time_zone
+ }
import settings
+try:
+ from collections import OrderedDict # @UnusedImport
+
+except:
+ from ordereddict import OrderedDict # @UnusedImport @Reimport
+
+
def pretty_date_time(date_time, tzinfo=None, short=False):
if date_time is None:
return '('+ _('never') + ')'
import re
import settings
+from config import additional_config, additional_section
-def get_wifi_settings():
+
+WPA_SUPPLICANT_CONF = settings.WPA_SUPPLICANT_CONF # @UndefinedVariable
+
+
+def _get_wifi_settings():
# will return the first configured network
-
- logging.debug('reading wifi settings from %s' % settings.WPA_SUPPLICANT_CONF)
+
+ logging.debug('reading wifi settings from %s' % WPA_SUPPLICANT_CONF)
try:
- conf_file = open(settings.WPA_SUPPLICANT_CONF, 'r')
+ conf_file = open(WPA_SUPPLICANT_CONF, 'r')
except Exception as e:
logging.error('could open wifi settings file %(path)s: %(msg)s' % {
- 'path': settings.WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
+ 'path': WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
return {
- 'ssid': None,
- 'psk': None
+ 'wifiEnabled': False,
+ 'wifiNetworkName': '',
+ 'wifiNetworkKey': ''
}
lines = conf_file.readlines()
logging.debug('wifi is disabled')
return {
- 'ssid': ssid,
- 'psk': psk
+ 'wifiEnabled': False,
+ 'wifiNetworkName': ssid,
+ 'wifiNetworkKey': psk
}
-def set_wifi_settings(s):
+def _set_wifi_settings(s):
# will update the first configured network
- logging.debug('writing wifi settings to %s' % settings.WPA_SUPPLICANT_CONF)
+ logging.debug('writing wifi settings to %s' % WPA_SUPPLICANT_CONF)
- enabled = bool(s['ssid'])
- ssid = s['ssid']
- psk = s['psk']
+ enabled = s['wifiEnabled']
+ ssid = s['wifiNetworkName']
+ psk = s['wifiNetworkKey']
try:
- conf_file = open(settings.WPA_SUPPLICANT_CONF, 'r')
+ conf_file = open(WPA_SUPPLICANT_CONF, 'r')
except Exception as e:
logging.error('could open wifi settings file %(path)s: %(msg)s' % {
- 'path': settings.WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
+ 'path': WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
return
lines.append('}\n\n')
try:
- conf_file = open(settings.WPA_SUPPLICANT_CONF, 'w')
+ conf_file = open(WPA_SUPPLICANT_CONF, 'w')
except Exception as e:
logging.error('could open wifi settings file %(path)s: %(msg)s' % {
- 'path': settings.WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
+ 'path': WPA_SUPPLICANT_CONF, 'msg': unicode(e)})
return
conf_file.write(line)
conf_file.close()
+
+
+@additional_section
+def network():
+ return {
+ 'label': 'Network',
+ 'description': 'configure the network connection',
+ 'advanced': True
+ }
+
+
+
+@additional_config
+def wifiEnabled():
+ if not WPA_SUPPLICANT_CONF:
+ return
+
+ return {
+ 'label': 'Wireless Network',
+ 'description': 'enable this if you want to connect to a wireless network',
+ 'type': 'bool',
+ 'section': 'network',
+ 'advanced': True,
+ 'reboot': True,
+ 'get': _get_wifi_settings,
+ 'set': _set_wifi_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def wifiNetworkName():
+ if not WPA_SUPPLICANT_CONF:
+ return
+
+ return {
+ 'label': 'Wireless Network Name',
+ 'description': 'the name (SSID) of your wireless network',
+ 'type': 'str',
+ 'section': 'network',
+ 'advanced': True,
+ 'required': True,
+ 'reboot': True,
+ 'depends': ['wifiEnabled'],
+ 'get': _get_wifi_settings,
+ 'set': _set_wifi_settings,
+ 'get_set_dict': True
+ }
+
+
+@additional_config
+def wifiNetworkKey():
+ if not WPA_SUPPLICANT_CONF:
+ return
+
+ return {
+ 'label': 'Wireless Network Key',
+ 'description': 'the key (PSK) required to connect to your wireless network',
+ 'type': 'pwd',
+ 'section': 'network',
+ 'advanced': True,
+ 'required': True,
+ 'reboot': True,
+ 'depends': ['wifiEnabled'],
+ 'get': _get_wifi_settings,
+ 'set': _set_wifi_settings,
+ 'get_set_dict': True
+ }
}
};
+Array.prototype.every = Array.prototype.every || function (callback, thisArg) {
+ for (var i = 0; i < this.length; i++) {
+ if (!callback.call(thisArg, this[i], i, this)) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
Array.prototype.unique = function (callback, thisArg) {
var uniqueElements = [];
this.forEach(function (element) {
});
};
+String.prototype.startsWith = String.prototype.startsWith || function (str) {
+ return (this.substr(0, str.length) === str);
+};
+
+String.prototype.endsWith = String.prototype.endsWith || function (str) {
+ return (this.substr(this.length - str.length) === str);
+};
+
+String.prototype.trim = String.prototype.trim || function () {
+ return this.replace(new RegExp('^\\s*'), '').replace(new RegExp('\\s*$'), '');
+};
+
String.prototype.replaceAll = String.prototype.replaceAll || function (oldStr, newStr) {
var p, s = this;
while ((p = s.indexOf(oldStr)) >= 0) {
function initUI() {
/* checkboxes */
- $('input[type=checkbox].styled').each(function () {
- makeCheckBox($(this));
- });
+ makeCheckBox($('input[type=checkbox].styled'));
/* sliders */
- makeSlider($('#brightnessSlider'), 0, 100, 2, null, 5, 0, '%');
- makeSlider($('#contrastSlider'), 0, 100, 2, null, 5, 0, '%');
- makeSlider($('#saturationSlider'), 0, 100, 2, null, 5, 0, '%');
- makeSlider($('#hueSlider'), 0, 100, 2, null, 5, 0, '%');
- makeSlider($('#framerateSlider'), 2, 30, 0, [
- {value: 1, label: '1'},
- {value: 5, label: '5'},
- {value: 10, label: '10'},
- {value: 15, label: '15'},
- {value: 20, label: '20'},
- {value: 25, label: '25'},
- {value: 30, label: '30'}
- ], null, 0);
- makeSlider($('#streamingFramerateSlider'), 1, 30, 0, [
- {value: 1, label: '1'},
- {value: 5, label: '5'},
- {value: 10, label: '10'},
- {value: 15, label: '15'},
- {value: 20, label: '20'},
- {value: 25, label: '25'},
- {value: 30, label: '30'}
- ], null, 0);
- makeSlider($('#streamingQualitySlider'), 0, 100, 2, null, 5, 0, '%');
- makeSlider($('#streamingResolutionSlider'), 0, 100, 2, null, 5, 0, '%');
- makeSlider($('#imageQualitySlider'), 0, 100, 2, null, 5, 0, '%');
- makeSlider($('#movieQualitySlider'), 0, 100, 2, null, 5, 0, '%');
- makeSlider($('#frameChangeThresholdSlider'), 0, 20, 0, null, 5, 1, '%');
-
- makeSlider($('#noiseLevelSlider'), 0, 25, 0, null, 6, 0, '%');
-
+ $('input[type=text].range.styled').each(function () {
+ var $this = $(this);
+ var $tr = $this.parent().parent();
+ var ticks = null;
+ var ticksAttr = $tr.attr('ticks');
+ if (ticksAttr) {
+ ticks = ticksAttr.split('|').map(function (t) {
+ var parts = t.split(',');
+ if (parts.length < 2) {
+ parts.push(parts[0]);
+ }
+ return {value: Number(parts[0]), label: parts[1]};
+ });
+ }
+ makeSlider($this, Number($tr.attr('min')), Number($tr.attr('max')),
+ Number($tr.attr('snap')), ticks, Number($tr.attr('ticksnum')), Number($tr.attr('decimals')), $tr.attr('unit'));
+ });
+
/* progress bars */
- makeProgressBar($('#diskUsageProgressBar'));
-
+ makeProgressBar($('div.progress-bar'));
+
/* text validators */
- makeTextValidator($('#adminUsernameEntry'), true);
- makeTextValidator($('#normalUsernameEntry'), true);
- makeTextValidator($('#wifiNameEntry'), true);
- makeTextValidator($('#deviceNameEntry'), true);
- makeTextValidator($('#networkServerEntry'), true);
- makeTextValidator($('#networkShareNameEntry'), true);
- makeTextValidator($('#networkUsernameEntry'), false);
- makeTextValidator($('#networkPasswordEntry'), false);
- makeTextValidator($('#rootDirectoryEntry'), true);
- makeTextValidator($('#leftTextEntry'), true);
- makeTextValidator($('#rightTextEntry'), true);
- makeTextValidator($('#imageFileNameEntry'), true);
- makeTextValidator($('#movieFileNameEntry'), true);
- makeTextValidator($('#emailAddressesEntry'), true);
- makeTextValidator($('#smtpServerEntry'), true);
- makeTextValidator($('#smtpAccountEntry'), true);
- makeTextValidator($('#smtpPasswordEntry'), true);
- makeTextValidator($('#webHookUrlEntry'), true);
- makeTextValidator($('#commandNotificationsEntry'), true);
-
+ makeTextValidator($('tr[required=true] input[type=text]'), true);
+ makeTextValidator($('tr[required=true] input[type=password]'), true);
+
/* number validators */
- makeNumberValidator($('#streamingPortEntry'), 1024, 65535, false, false, true);
- makeNumberValidator($('#snapshotIntervalEntry'), 1, 86400, false, false, true);
- makeNumberValidator($('#picturesLifetimeEntry'), 1, 3650, false, false, true);
- makeNumberValidator($('#moviesLifetimeEntry'), 1, 3650, false, false, true);
- makeNumberValidator($('#eventGapEntry'), 1, 86400, false, false, true);
- makeNumberValidator($('#preCaptureEntry'), 0, 100, false, false, true);
- makeNumberValidator($('#postCaptureEntry'), 0, 100, false, false, true);
- makeNumberValidator($('#minimumMotionFramesEntry'), 1, 1000, false, false, true);
- makeNumberValidator($('#smtpPortEntry'), 1, 65535, false, false, true);
- makeNumberValidator($('#emailPictureTimeSpanEntry'), 0, 60, false, false, true);
-
+ $('input[type=text].number').each(function () {
+ var $this = $(this);
+ var $tr = $this.parent().parent();
+ makeTextValidator($this, Number($tr.attr('min')), Number($tr.attr('max')),
+ Boolean($tr.attr('floating')), Boolean($tr.attr('sign')), Boolean($tr.attr('required')));
+ });
+
/* time validators */
- makeTimeValidator($('#mondayFromEntry'));
- makeTimeValidator($('#mondayToEntry'));
- makeTimeValidator($('#tuesdayFromEntry'));
- makeTimeValidator($('#tuesdayToEntry'));
- makeTimeValidator($('#wednesdayFromEntry'));
- makeTimeValidator($('#wednesdayToEntry'));
- makeTimeValidator($('#thursdayFromEntry'));
- makeTimeValidator($('#thursdayToEntry'));
- makeTimeValidator($('#fridayFromEntry'));
- makeTimeValidator($('#fridayToEntry'));
- makeTimeValidator($('#saturdayFromEntry'));
- makeTimeValidator($('#saturdayToEntry'));
- makeTimeValidator($('#sundayFromEntry'));
- makeTimeValidator($('#sundayToEntry'));
+ makeTimeValidator($('input[type=text].time'));
/* custom validators */
makeCustomValidator($('#rootDirectoryEntry'), function (value) {
- if ($('#storageDeviceSelect').val() == 'custom-path' && $.trim(value) == '/') {
+ if ($('#storageDeviceSelect').val() == 'custom-path' && String(value).trim() == '/') {
return 'files cannot be created directly on the root of your system';
}
}, '');
/* input value processors */
-
- makeStrippedInput($('#adminUsernameEntry'));
- makeStrippedInput($('#adminPasswordEntry'));
- makeStrippedInput($('#normalUsernameEntry'));
- makeStrippedInput($('#normalPasswordEntry'));
- makeStrippedInput($('#deviceNameEntry'));
- makeStrippedInput($('#rootDirectoryEntry'));
- makeStrippedInput($('#leftTextEntry'));
- makeStrippedInput($('#rightTextEntry'));
- makeStrippedInput($('#imageFileNameEntry'));
- makeStrippedInput($('#movieFileNameEntry'));
- makeStrippedInput($('#emailAddressesEntry'));
- makeStrippedInput($('#smtpServerEntry'));
- makeStrippedInput($('#smtpAccountEntry'));
- makeStrippedInput($('#smtpPasswordEntry'));
- makeStrippedInput($('#webHookUrlEntry'));
- makeStrippedInput($('#commandNotificationsEntry'));
-
+ makeStrippedInput($('tr[strip=true] input[type=text]'));
+ makeStrippedInput($('tr[strip=true] input[type=password]'));
+
/* ui elements that enable/disable other ui elements */
$('#motionEyeSwitch').change(updateConfigUi);
$('#showAdvancedSwitch').change(updateConfigUi);
- $('#wifiSwitch').change(updateConfigUi);
$('#storageDeviceSelect').change(updateConfigUi);
$('#resolutionSelect').change(updateConfigUi);
$('#leftTextSelect').change(updateConfigUi);
$('#fridayEnabledSwitch').change(updateConfigUi);
$('#saturdayEnabledSwitch').change(updateConfigUi);
$('#sundayEnabledSwitch').change(updateConfigUi);
+
+ /* additional configs */
+ var seenDependNames = {};
+ $('tr[depends]').each(function () {
+ var $tr = $(this);
+ var depends = $tr.attr('depends').split(' ');
+ depends.forEach(function (depend) {
+ if (depend.charAt(0) == '!') {
+ depend = depend.substring(1);
+ }
+
+ if (depend in seenDependNames) {
+ return;
+ }
+
+ seenDependNames[depend] = true;
+
+ var control = $('#' + depend + 'Entry, #' + depend + 'Select, #' + depend + 'Slider, #' + depend + 'Switch');
+ control.change(updateConfigUi);
+ });
+ });
$('#storageDeviceSelect').change(function () {
$('#rootDirectoryEntry').val('/');
});
$('#rootDirectoryEntry').change(function () {
- this.value = $.trim(this.value);
+ this.value = this.value.trim();
});
$('#rootDirectoryEntry').change(function () {
fetchCurrentCameraConfig(endProgress);
}
});
- $('input.general, select.general').change(pushMainConfig);
- $('input.wifi').change(pushMainConfig);
- $('input.device, select.device, ' +
- 'input.storage, select.storage, ' +
- 'input.text-overlay, select.text-overlay, ' +
- 'input.streaming, select.streaming, ' +
- 'input.still-images, select.still-images, ' +
- 'input.motion-detection, select.motion-detection, ' +
- 'input.motion-movies, select.motion-movies, ' +
- 'input.notifications, select.notifications, ' +
- 'input.working-schedule, select.working-schedule').change(pushCameraConfig);
+ $('input.main-config, select.main-config').change(pushMainConfig);
+ $('input.camera-config, select.camera-config').change(pushCameraConfig);
/* preview controls */
$('#brightnessSlider').change(function () {pushPreview('brightness');});
/* logout button */
$('div.button.logout-button').click(doLogout);
- /* read-only entries */
+ /* autoselect urls in read-only entries */
$('#streamingSnapshotUrlEntry:text, #streamingMjpgUrlEntry:text, #streamingEmbedUrlEntry:text').click(function () {
this.select();
});
objs.not($('#motionEyeSwitch').parents('div').get(0)).each(markHide);
}
- /* wifi switch */
- if (!$('#wifiSwitch').get(0).checked) {
- $('#wifiSwitch').parent().next('table.settings').find('tr.settings-item').each(markHide);
- }
-
if ($('#cameraSelect').find('option').length < 2) { /* no camera configured */
$('#videoDeviceSwitch').parent().each(markHide);
$('#videoDeviceSwitch').parent().nextAll('div.settings-section-title, table.settings').each(markHide);
$('#workingScheduleSwitch').parent().next('table.settings').find('tr.settings-item').each(markHide);
}
+ /* additional configs */
+ $('tr[depends]').each(function () {
+ var $tr = $(this);
+ var depends = $tr.attr('depends').split(' ');
+ var conditionOk = true;
+ depends.every(function (depend) {
+ var neg = false;
+ if (depend.charAt(0) == '!') {
+ neg = true;
+ depend = depend.substring(1);
+ }
+
+ var control = $('#' + depend + 'Entry, #' + depend + 'Select, #' + depend + 'Slider');
+ var val = false;
+ if (control.length) {
+ val = control.val();
+ }
+ else { /* maybe it's a checkbox */
+ control = $('#' + depend + 'Switch');
+ if (control.length) {
+ val = control.get(0).checked;
+ }
+ }
+
+ val = Boolean(val);
+ if (neg) {
+ val = !val;
+ }
+
+ if (!val) {
+ conditionOk = false;
+ return false;
+ }
+
+ return true;
+ });
+
+ if (!conditionOk) {
+ $tr.each(markHide);
+ }
+ });
+
var weekDays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
weekDays.forEach(function (weekDay) {
var check = $('#' + weekDay + 'EnabledSwitch');
}
function mainUi2Dict() {
- return {
+ var dict = {
'enabled': $('#motionEyeSwitch')[0].checked,
'show_advanced': $('#showAdvancedSwitch')[0].checked,
'admin_username': $('#adminUsernameEntry').val(),
'admin_password': $('#adminPasswordEntry').val(),
'normal_username': $('#normalUsernameEntry').val(),
- 'normal_password': $('#normalPasswordEntry').val(),
- 'time_zone': $('#timeZoneSelect').val(),
-
- 'wifi_enabled': $('#wifiSwitch')[0].checked,
- 'wifi_name': $('#wifiNameEntry').val(),
- 'wifi_key': $('#wifiKeyEntry').val()
+ 'normal_password': $('#normalPasswordEntry').val()
};
+
+ /* additional sections */
+ $('input[type=checkbox].additional-section.main-config').each(function () {
+ dict['_' + this.id.substring(0, this.id.length - 6)] = this.checked;
+ });
+
+ /* additional configs */
+ $('tr.additional-config').each(function () {
+ var $this = $(this);
+ var control = $this.find('input, select');
+
+ if (!control.hasClass('main-config')) {
+ return;
+ }
+
+ var id = control.attr('id');
+ var name, value;
+ if (id.endsWith('Entry')) {
+ name = id.substring(0, id.length - 5);
+ value = control.val();
+ }
+ else if (id.endsWith('Select')) {
+ name = id.substring(0, id.length - 6);
+ value = control.val();
+ }
+ else if (id.endsWith('Slider')) {
+ name = id.substring(0, id.length - 6);
+ value = control.val();
+ }
+ else if (id.endsWith('Switch')) {
+ name = id.substring(0, id.length - 6);
+ value = control[0].checked;
+ }
+
+ dict['_' + name] = value;
+ });
+
+ return dict;
}
function dict2MainUi(dict) {
$('#adminPasswordEntry').val(dict['admin_password']);
$('#normalUsernameEntry').val(dict['normal_username']);
$('#normalPasswordEntry').val(dict['normal_password']);
- $('#timeZoneSelect').val(dict['time_zone']);
-
- $('#wifiSwitch')[0].checked = dict['wifi_enabled'];
- $('#wifiNameEntry').val(dict['wifi_name']);
- $('#wifiKeyEntry').val(dict['wifi_key']);
-
+
+ /* additional sections */
+ $('input[type=checkbox].additional-section.main-config').each(function () {
+ this.checked = dict['_' + this.id.substring(0, this.id.length - 6)];
+ });
+
+ /* additional configs */
+ $('tr.additional-config').each(function () {
+ var $this = $(this);
+ var control = $this.find('input, select');
+
+ if (!control.hasClass('main-config')) {
+ return;
+ }
+
+ var id = control.attr('id');
+ var name;
+ if (id.endsWith('Entry')) {
+ name = id.substring(0, id.length - 5);
+ control.val(dict['_' + name]);
+ }
+ else if (id.endsWith('Select')) {
+ name = id.substring(0, id.length - 6);
+ control.val(dict['_' + name]);
+ }
+ else if (id.endsWith('Slider')) {
+ name = id.substring(0, id.length - 6);
+ control.val(dict['_' + name]);
+ }
+ else if (id.endsWith('Switch')) {
+ name = id.substring(0, id.length - 6);
+ control[0].checked = dict['_' + name];
+ }
+ });
+
updateConfigUi();
}
dict.hue = $('#hueSlider').val();
}
+ /* additional sections */
+ $('input[type=checkbox].additional-section.camera-config').each(function () {
+ dict['_' + this.id.substring(0, this.id.length - 6)] = this.checked;
+ });
+
+ /* additional configs */
+ $('tr.additional-config').each(function () {
+ var $this = $(this);
+ var control = $this.find('input, select');
+
+ if (!control.hasClass('camera-config')) {
+ return;
+ }
+
+ var id = control.attr('id');
+ var name, value;
+ if (id.endsWith('Entry')) {
+ name = id.substring(0, id.length - 5);
+ value = control.val();
+ }
+ else if (id.endsWith('Select')) {
+ name = id.substring(0, id.length - 6);
+ value = control.val();
+ }
+ else if (id.endsWith('Slider')) {
+ name = id.substring(0, id.length - 6);
+ value = control.val();
+ }
+ else if (id.endsWith('Switch')) {
+ name = id.substring(0, id.length - 6);
+ value = control[0].checked;
+ }
+
+ dict['_' + name] = value;
+ });
+
return dict;
}
$('#sundayToEntry').val(dict['sunday_to']);
$('#workingScheduleTypeSelect').val(dict['working_schedule_type']);
+ /* additional sections */
+ $('input[type=checkbox].additional-section.main-config').each(function () {
+ this.checked = dict[this.id.substring(0, this.id.length - 6)];
+ });
+
+ /* additional configs */
+ $('tr.additional-config').each(function () {
+ var $this = $(this);
+ var control = $this.find('input, select');
+
+ if (!control.hasClass('camera-config')) {
+ return;
+ }
+
+ var id = control.attr('id');
+ var name;
+ if (id.endsWith('Entry')) {
+ name = id.substring(0, id.length - 5);
+ control.val(dict['_' + name]);
+ }
+ else if (id.endsWith('Select')) {
+ name = id.substring(0, id.length - 6);
+ control.val(dict['_' + name]);
+ }
+ else if (id.endsWith('Slider')) {
+ name = id.substring(0, id.length - 6);
+ control.val(dict['_' + name]);
+ }
+ else if (id.endsWith('Switch')) {
+ name = id.substring(0, id.length - 6);
+ control[0].checked = dict['_' + name];
+ }
+ });
+
updateConfigUi();
}
/* UI widgets */
function makeCheckBox($input) {
- if (!$input.length) {
- return;
- }
-
- var mainDiv = $('<div class="check-box"></div>');
- var buttonDiv = $('<div class="check-box-button"></div>');
- var text = $('<span class="check-box-text"><span>');
-
- function setOn() {
- text.html('ON');
- mainDiv.addClass('on');
- }
-
- function setOff() {
- text.html('OFF');
- mainDiv.removeClass('on');
- }
-
- buttonDiv.append(text);
- mainDiv.append(buttonDiv);
-
- /* transfer the CSS classes */
- mainDiv[0].className += ' ' + $input[0].className;
-
- /* add the element */
- $input.after(mainDiv);
-
- function update() {
- if ($input[0].checked) {
- setOn();
+ $input.each(function () {
+ var $this = $(this);
+
+ var mainDiv = $('<div class="check-box"></div>');
+ var buttonDiv = $('<div class="check-box-button"></div>');
+ var text = $('<span class="check-box-text"><span>');
+
+ function setOn() {
+ text.html('ON');
+ mainDiv.addClass('on');
}
- else {
- setOff();
+
+ function setOff() {
+ text.html('OFF');
+ mainDiv.removeClass('on');
}
- }
-
- /* add event handers */
- $input.change(update).change();
-
- mainDiv.click(function () {
- $input[0].checked = !$input[0].checked;
- $input.change();
- });
-
- /* make the element focusable */
- mainDiv[0].tabIndex = 0;
-
- /* handle the key events */
- mainDiv.keydown(function (e) {
- if (e.which === 13 || e.which === 32) {
- $input[0].checked = !$input[0].checked;
- $input.change();
-
- return false;
+
+ buttonDiv.append(text);
+ mainDiv.append(buttonDiv);
+
+ /* transfer the CSS classes */
+ mainDiv[0].className += ' ' + $this[0].className;
+
+ /* add the element */
+ $this.after(mainDiv);
+
+ function update() {
+ if ($this[0].checked) {
+ setOn();
+ }
+ else {
+ setOff();
+ }
}
+
+ /* add event handers */
+ $this.change(update).change();
+
+ mainDiv.click(function () {
+ $this[0].checked = !$this[0].checked;
+ $this.change();
+ });
+
+ /* make the element focusable */
+ mainDiv[0].tabIndex = 0;
+
+ /* handle the key events */
+ mainDiv.keydown(function (e) {
+ if (e.which === 13 || e.which === 32) {
+ $this[0].checked = !$this[0].checked;
+ $this.change();
+
+ return false;
+ }
+ });
+
+ this.update = update;
});
-
- $input[0].update = update;
-
- return mainDiv;
}
function makeSlider($input, minVal, maxVal, snapMode, ticks, ticksNumber, decimals, unit) {
- if (!$input.length) {
- return;
- }
-
unit = unit || '';
-
- var slider = $('<div class="slider"></div>');
-
- var labels = $('<div class="slider-labels"></div>');
- slider.append(labels);
-
- var bar = $('<div class="slider-bar"></div>');
- slider.append(bar);
-
- bar.append('<div class="slider-bar-inside"></div>');
-
- var cursor = $('<div class="slider-cursor"></div>');
- bar.append(cursor);
-
- var cursorLabel = $('<div class="slider-cursor-label"></div>');
- cursor.append(cursorLabel);
-
- function bestPos(pos) {
- if (pos < 0) {
- pos = 0;
- }
- if (pos > 100) {
- pos = 100;
- }
+
+ $input.each(function () {
+ var $this = $(this);
+ var slider = $('<div class="slider"></div>');
- if (snapMode > 0) {
- var minDif = Infinity;
- var bestPos = null;
- for (var i = 0; i < ticks.length; i++) {
- var tick = ticks[i];
- var p = valToPos(tick.value);
- var dif = Math.abs(p - pos);
- if ((dif < minDif) && (snapMode == 1 || dif < 5)) {
- minDif = dif;
- bestPos = p;
- }
+ var labels = $('<div class="slider-labels"></div>');
+ slider.append(labels);
+
+ var bar = $('<div class="slider-bar"></div>');
+ slider.append(bar);
+
+ bar.append('<div class="slider-bar-inside"></div>');
+
+ var cursor = $('<div class="slider-cursor"></div>');
+ bar.append(cursor);
+
+ var cursorLabel = $('<div class="slider-cursor-label"></div>');
+ cursor.append(cursorLabel);
+
+ function bestPos(pos) {
+ if (pos < 0) {
+ pos = 0;
+ }
+ if (pos > 100) {
+ pos = 100;
}
- if (bestPos != null) {
- pos = bestPos;
+ if (snapMode > 0) {
+ var minDif = Infinity;
+ var bestPos = null;
+ for (var i = 0; i < ticks.length; i++) {
+ var tick = ticks[i];
+ var p = valToPos(tick.value);
+ var dif = Math.abs(p - pos);
+ if ((dif < minDif) && (snapMode == 1 || dif < 5)) {
+ minDif = dif;
+ bestPos = p;
+ }
+ }
+
+ if (bestPos != null) {
+ pos = bestPos;
+ }
}
- }
-
- return pos;
- }
-
- function getPos() {
- return parseInt(cursor.position().left * 100 / bar.width());
- }
-
- function valToPos(val) {
- return (val - minVal) * 100 / (maxVal - minVal);
- }
-
- function posToVal(pos) {
- return minVal + pos * (maxVal - minVal) / 100;
- }
-
- function sliderChange(val) {
- $input.val(val.toFixed(decimals));
- cursorLabel.html('' + val.toFixed(decimals) + unit);
- }
-
- function bodyMouseMove(e) {
- if (bar[0]._mouseDown) {
- var offset = bar.offset();
- var pos = e.pageX - offset.left - 5;
- pos = pos / slider.width() * 100;
- pos = bestPos(pos);
- var val = posToVal(pos);
- cursor.css('left', pos + '%');
- sliderChange(val);
+ return pos;
}
- }
-
- function bodyMouseUp(e) {
- bar[0]._mouseDown = false;
-
- $('body').unbind('mousemove', bodyMouseMove);
- $('body').unbind('mouseup', bodyMouseUp);
-
- cursorLabel.css('display', 'none');
- $input.change();
- }
-
- bar.mousedown(function (e) {
- if (e.which > 1) {
- return;
+ function getPos() {
+ return parseInt(cursor.position().left * 100 / bar.width());
}
- this._mouseDown = true;
- bodyMouseMove(e);
-
- $('body').mousemove(bodyMouseMove);
- $('body').mouseup(bodyMouseUp);
+ function valToPos(val) {
+ return (val - minVal) * 100 / (maxVal - minVal);
+ }
- slider.focus();
- cursorLabel.css('display', 'inline-block');
+ function posToVal(pos) {
+ return minVal + pos * (maxVal - minVal) / 100;
+ }
- return false;
- });
-
- /* ticks */
- var autoTicks = (ticks == null);
-
- function makeTicks() {
- if (ticksNumber == null) {
- ticksNumber = 11;
+ function sliderChange(val) {
+ $this.val(val.toFixed(decimals));
+ cursorLabel.html('' + val.toFixed(decimals) + unit);
}
-
- labels.html('');
-
- if (autoTicks) {
- ticks = [];
- var i;
- for (i = 0; i < ticksNumber; i++) {
- var val = minVal + i * (maxVal - minVal) / (ticksNumber - 1);
- var valStr;
- if (Math.round(val) == val) {
- valStr = '' + val;
- }
- else {
- valStr = val.toFixed(decimals);
- }
- ticks.push({value: val, label: valStr + unit});
+
+ function bodyMouseMove(e) {
+ if (bar[0]._mouseDown) {
+ var offset = bar.offset();
+ var pos = e.pageX - offset.left - 5;
+ pos = pos / slider.width() * 100;
+ pos = bestPos(pos);
+ var val = posToVal(pos);
+
+ cursor.css('left', pos + '%');
+ sliderChange(val);
}
}
- for (i = 0; i < ticks.length; i++) {
- var tick = ticks[i];
- var pos = valToPos(tick.value);
- var span = $('<span class="slider-label" style="left: -9999px;">' + tick.label + '</span>');
+ function bodyMouseUp(e) {
+ bar[0]._mouseDown = false;
+
+ $('body').unbind('mousemove', bodyMouseMove);
+ $('body').unbind('mouseup', bodyMouseUp);
+
+ cursorLabel.css('display', 'none');
- labels.append(span);
- span.css('left', (pos - 10) + '%');
+ $this.change();
}
- return ticks;
- }
+ bar.mousedown(function (e) {
+ if (e.which > 1) {
+ return;
+ }
+
+ this._mouseDown = true;
+ bodyMouseMove(e);
- makeTicks();
-
- function input2slider() {
- var value = parseFloat($input.val());
- if (isNaN(value)) {
- value = minVal;
- }
+ $('body').mousemove(bodyMouseMove);
+ $('body').mouseup(bodyMouseUp);
+
+ slider.focus();
+ cursorLabel.css('display', 'inline-block');
+
+ return false;
+ });
- var pos = valToPos(value);
- pos = bestPos(pos);
- cursor.css('left', pos + '%');
- cursorLabel.html($input.val() + unit);
- }
-
- /* transfer the CSS classes */
- slider.addClass($input.attr('class'));
-
- /* handle input events */
- $input.change(input2slider).change();
-
- /* add the slider to the parent of the input */
- $input.after(slider);
-
- /* make the slider focusable */
- slider.attr('tabIndex', 0);
+ /* ticks */
+ var autoTicks = (ticks == null);
+
+ function makeTicks() {
+ if (ticksNumber == null) {
+ ticksNumber = 11;
+ }
- /* handle key events */
- slider.keydown(function (e) {
- switch (e.which) {
- case 37: /* left */
- if (snapMode == 1) { /* strict snapping */
- // TODO implement me
- }
- else {
- var step = (maxVal - minVal) / 200;
- var val = Math.max(minVal, parseFloat($input.val()) - step);
- if (decimals == 0) {
- val = Math.floor(val);
+ labels.html('');
+
+ if (autoTicks) {
+ ticks = [];
+ var i;
+ for (i = 0; i < ticksNumber; i++) {
+ var val = minVal + i * (maxVal - minVal) / (ticksNumber - 1);
+ var valStr;
+ if (Math.round(val) == val) {
+ valStr = '' + val;
}
-
- var origSnapMode = snapMode;
- snapMode = 0;
- $input.val(val).change();
- snapMode = origSnapMode;
- }
-
- break;
-
- case 39: /* right */
- if (snapMode == 1) { /* strict snapping */
- // TODO implement me
- }
- else {
- var step = (maxVal - minVal) / 200;
- var val = Math.min(maxVal, parseFloat($input.val()) + step);
- if (decimals == 0) {
- val = Math.ceil(val);
+ else {
+ valStr = val.toFixed(decimals);
}
-
- var origSnapMode = snapMode;
- snapMode = 0;
- $input.val(val).change();
- snapMode = origSnapMode;
+ ticks.push({value: val, label: valStr + unit});
}
+ }
+
+ for (i = 0; i < ticks.length; i++) {
+ var tick = ticks[i];
+ var pos = valToPos(tick.value);
+ var span = $('<span class="slider-label" style="left: -9999px;">' + tick.label + '</span>');
- break;
+ labels.append(span);
+ span.css('left', (pos - 10) + '%');
+ }
+
+ return ticks;
}
- });
+
+ makeTicks();
- $input.each(function () {
+ function input2slider() {
+ var value = parseFloat($this.val());
+ if (isNaN(value)) {
+ value = minVal;
+ }
+
+ var pos = valToPos(value);
+ pos = bestPos(pos);
+ cursor.css('left', pos + '%');
+ cursorLabel.html($this.val() + unit);
+ }
+
+ /* transfer the CSS classes */
+ slider.addClass($this.attr('class'));
+
+ /* handle input events */
+ $this.change(input2slider).change();
+
+ /* add the slider to the parent of the input */
+ $this.after(slider);
+
+ /* make the slider focusable */
+ slider.attr('tabIndex', 0);
+
+ /* handle key events */
+ slider.keydown(function (e) {
+ switch (e.which) {
+ case 37: /* left */
+ if (snapMode == 1) { /* strict snapping */
+ // TODO implement me
+ }
+ else {
+ var step = (maxVal - minVal) / 200;
+ var val = Math.max(minVal, parseFloat($this.val()) - step);
+ if (decimals == 0) {
+ val = Math.floor(val);
+ }
+
+ var origSnapMode = snapMode;
+ snapMode = 0;
+ $this.val(val).change();
+ snapMode = origSnapMode;
+ }
+
+ break;
+
+ case 39: /* right */
+ if (snapMode == 1) { /* strict snapping */
+ // TODO implement me
+ }
+ else {
+ var step = (maxVal - minVal) / 200;
+ var val = Math.min(maxVal, parseFloat($this.val()) + step);
+ if (decimals == 0) {
+ val = Math.ceil(val);
+ }
+
+ var origSnapMode = snapMode;
+ snapMode = 0;
+ $this.val(val).change();
+ snapMode = origSnapMode;
+ }
+
+ break;
+ }
+ });
+
this.update = input2slider;
- });
-
- slider[0].setMinVal = function (mv) {
- minVal = mv;
-
- makeTicks();
- };
+
+ slider[0].setMinVal = function (mv) {
+ minVal = mv;
- slider[0].setMaxVal = function (mv) {
- maxVal = mv;
-
- makeTicks();
+ makeTicks();
+ };
- input2slider();
- };
+ slider[0].setMaxVal = function (mv) {
+ maxVal = mv;
- return slider;
+ makeTicks();
+
+ input2slider();
+ };
+ });
}
function makeProgressBar($div) {
- if (!$div.length) {
- return;
- }
-
- $div.addClass('progress-bar-container');
- var fillDiv = $('<div class="progress-bar-fill"></div>');
- var textSpan = $('<span class="progress-bar-text"></span>');
-
- $div.append(fillDiv);
- $div.append(textSpan);
-
- $div[0].setProgress = function (progress) {
- $div.progress = progress;
- fillDiv.width(progress + '%');
- };
+ $div.each(function () {
+ var $this = $(this);
+
+ $this.addClass('progress-bar-container');
+ var fillDiv = $('<div class="progress-bar-fill"></div>');
+ var textSpan = $('<span class="progress-bar-text"></span>');
- $div[0].setText = function (text) {
- textSpan.html(text);
- };
-
- return $div;
+ $this.append(fillDiv);
+ $this.append(textSpan);
+
+ this.setProgress = function (progress) {
+ $this.progress = progress;
+ fillDiv.width(progress + '%');
+ };
+
+ this.setText = function (text) {
+ textSpan.html(text);
+ };
+ });
}
/* validators */
function makeTextValidator($input, required) {
- if (!$input.length) {
- return;
- }
-
if (required == null) {
required = true;
}
-
- function isValid(strVal) {
- if (!$input.is(':visible')) {
- return true; /* an invisible element is considered always valid */
- }
-
- if (strVal.length === 0 && required) {
- return false;
- }
- return true;
- }
-
- var msg = 'this field is required';
+ $input.each(function () {
+ var $this = $(this);
+
+ function isValid(strVal) {
+ if (!$this.is(':visible')) {
+ return true; /* an invisible element is considered always valid */
+ }
+
+ if (strVal.length === 0 && required) {
+ return false;
+ }
- function validate() {
- var strVal = $input.val();
- if (isValid(strVal)) {
- $input.attr('title', '');
- $input.removeClass('error');
- $input[0].invalid = false;
- }
- else {
- $input.attr('title', msg);
- $input.addClass('error');
- $input[0].invalid = true;
+ return true;
}
- }
-
- $input.keyup(validate);
- $input.blur(validate);
- $input.change(validate).change();
-
- $input.addClass('validator');
- $input.addClass('text-validator');
- $input.each(function () {
- var oldValidate = this.validate;
- this.validate = function () {
- if (oldValidate) {
- oldValidate.call(this);
+
+ var msg = 'this field is required';
+
+ function validate() {
+ var strVal = $this.val();
+ if (isValid(strVal)) {
+ $this.attr('title', '');
+ $this.removeClass('error');
+ $this[0].invalid = false;
+ }
+ else {
+ $this.attr('title', msg);
+ $this.addClass('error');
+ $this[0].invalid = true;
}
- validate();
}
+
+ $this.keyup(validate);
+ $this.blur(validate);
+ $this.change(validate).change();
+
+ $this.addClass('validator');
+ $this.addClass('text-validator');
+ $this.each(function () {
+ var oldValidate = this.validate;
+ this.validate = function () {
+ if (oldValidate) {
+ oldValidate.call(this);
+ }
+ validate();
+ }
+ });
});
}
function makeComboValidator($select, required) {
- if (!$select.length) {
- return;
- }
-
if (required == null) {
required = true;
}
-
- function isValid(strVal) {
- if (!$select.is(':visible')) {
- return true; /* an invisible element is considered always valid */
- }
-
- if (strVal.length === 0 && required) {
- return false;
- }
- return true;
- }
-
- var msg = 'this field is required';
+ $select.each(function () {
+ $this = $(this);
+
+ function isValid(strVal) {
+ if (!$this.is(':visible')) {
+ return true; /* an invisible element is considered always valid */
+ }
+
+ if (strVal.length === 0 && required) {
+ return false;
+ }
- function validate() {
- var strVal = $select.val() || '';
- if (isValid(strVal)) {
- $select.attr('title', '');
- $select.removeClass('error');
- $select[0].invalid = false;
- }
- else {
- $select.attr('title', msg);
- $select.addClass('error');
- $select[0].invalid = true;
+ return true;
}
- }
-
- $select.keyup(validate);
- $select.blur(validate);
- $select.change(validate).change();
-
- $select.addClass('validator');
- $select.addClass('combo-validator');
- $select.each(function () {
- var oldValidate = this.validate;
- this.validate = function () {
- if (oldValidate) {
- oldValidate.call(this);
+
+ var msg = 'this field is required';
+
+ function validate() {
+ var strVal = $this.val() || '';
+ if (isValid(strVal)) {
+ $this.attr('title', '');
+ $this.removeClass('error');
+ $this[0].invalid = false;
+ }
+ else {
+ $this.attr('title', msg);
+ $this.addClass('error');
+ $this[0].invalid = true;
}
- validate();
}
+
+ $this.keyup(validate);
+ $this.blur(validate);
+ $this.change(validate).change();
+
+ $this.addClass('validator');
+ $this.addClass('combo-validator');
+ $this.each(function () {
+ var oldValidate = this.validate;
+ this.validate = function () {
+ if (oldValidate) {
+ oldValidate.call(this);
+ }
+ validate();
+ }
+ });
});
}
function makeNumberValidator($input, minVal, maxVal, floating, sign, required) {
- if (!$input.length) {
- return;
- }
-
if (minVal == null) {
minVal = -Infinity;
}
if (required == null) {
required = true;
}
-
- function isValid(strVal) {
- if (!$input.is(':visible')) {
- return true; /* an invisible element is considered always valid */
- }
- if (strVal.length === 0 && !required) {
+ $input.each(function () {
+ var $this = $(this);
+
+ function isValid(strVal) {
+ if (!$this.is(':visible')) {
+ return true; /* an invisible element is considered always valid */
+ }
+
+ if (strVal.length === 0 && !required) {
+ return true;
+ }
+
+ var numVal = parseInt(strVal);
+ if ('' + numVal != strVal) {
+ return false;
+ }
+
+ if (numVal < minVal || numVal > maxVal) {
+ return false;
+ }
+
+ if (!sign && numVal < 0) {
+ return false;
+ }
+
return true;
}
- var numVal = parseInt(strVal);
- if ('' + numVal != strVal) {
- return false;
+ var msg = '';
+ if (!sign) {
+ msg = 'enter a positive';
}
-
- if (numVal < minVal || numVal > maxVal) {
- return false;
+ else {
+ msg = 'enter a';
}
-
- if (!sign && numVal < 0) {
- return false;
- }
-
- return true;
- }
-
- var msg = '';
- if (!sign) {
- msg = 'enter a positive';
- }
- else {
- msg = 'enter a';
- }
- if (floating) {
- msg += ' number';
- }
- else {
- msg += ' integer number';
- }
- if (isFinite(minVal)) {
- if (isFinite(maxVal)) {
- msg += ' between ' + minVal + ' and ' + maxVal;
+ if (floating) {
+ msg += ' number';
}
else {
- msg += ' greater than ' + minVal;
- }
- }
- else {
- if (isFinite(maxVal)) {
- msg += ' smaller than ' + maxVal;
+ msg += ' integer number';
}
- }
-
- function validate() {
- var strVal = $input.val();
- if (isValid(strVal)) {
- $input.attr('title', '');
- $input.removeClass('error');
- $input[0].invalid = false;
+ if (isFinite(minVal)) {
+ if (isFinite(maxVal)) {
+ msg += ' between ' + minVal + ' and ' + maxVal;
+ }
+ else {
+ msg += ' greater than ' + minVal;
+ }
}
else {
- $input.attr('title', msg);
- $input.addClass('error');
- $input[0].invalid = true;
+ if (isFinite(maxVal)) {
+ msg += ' smaller than ' + maxVal;
+ }
}
- }
-
- $input.keyup(validate);
- $input.blur(validate);
- $input.change(validate).change();
-
- $input.addClass('validator');
- $input.addClass('number-validator');
- $input.each(function () {
- var oldValidate = this.validate;
- this.validate = function () {
- if (oldValidate) {
- oldValidate.call(this);
+
+ function validate() {
+ var strVal = $this.val();
+ if (isValid(strVal)) {
+ $this.attr('title', '');
+ $this.removeClass('error');
+ $this[0].invalid = false;
+ }
+ else {
+ $this.attr('title', msg);
+ $this.addClass('error');
+ $this[0].invalid = true;
}
- validate();
}
+
+ $this.keyup(validate);
+ $this.blur(validate);
+ $this.change(validate).change();
+
+ $this.addClass('validator');
+ $this.addClass('number-validator');
+ $this.each(function () {
+ var oldValidate = this.validate;
+ this.validate = function () {
+ if (oldValidate) {
+ oldValidate.call(this);
+ }
+ validate();
+ }
+ });
});
makeStrippedInput($input);
}
function makeTimeValidator($input) {
- if (!$input.length) {
- return;
- }
-
- function isValid(strVal) {
- if (!$input.is(':visible')) {
- return true; /* an invisible element is considered always valid */
- }
+ $input.each(function () {
+ var $this = $(this);
- return strVal.match(new RegExp('^[0-2][0-9]:[0-5][0-9]$')) != null;
- }
-
- var msg = 'enter a valid time in the following format: HH:MM';
+ function isValid(strVal) {
+ if (!$this.is(':visible')) {
+ return true; /* an invisible element is considered always valid */
+ }
- function validate() {
- var strVal = $input.val();
- if (isValid(strVal)) {
- $input.attr('title', '');
- $input.removeClass('error');
- $input[0].invalid = false;
- }
- else {
- $input.attr('title', msg);
- $input.addClass('error');
- $input[0].invalid = true;
+ return strVal.match(new RegExp('^[0-2][0-9]:[0-5][0-9]$')) != null;
}
- }
-
- $input.keyup(validate);
- $input.blur(validate);
- $input.change(validate).change();
- $input.timepicker({
- closeOnWindowScroll: true,
- selectOnBlur: true,
- timeFormat: 'H:i',
- });
-
- $input.addClass('validator');
- $input.addClass('time-validator');
- $input.each(function () {
- var oldValidate = this.validate;
- this.validate = function () {
- if (oldValidate) {
- oldValidate.call(this);
+
+ var msg = 'enter a valid time in the following format: HH:MM';
+
+ function validate() {
+ var strVal = $this.val();
+ if (isValid(strVal)) {
+ $this.attr('title', '');
+ $this.removeClass('error');
+ $this[0].invalid = false;
+ }
+ else {
+ $this.attr('title', msg);
+ $this.addClass('error');
+ $this[0].invalid = true;
}
- validate();
}
+
+ $this.keyup(validate);
+ $this.blur(validate);
+ $this.change(validate).change();
+ $this.timepicker({
+ closeOnWindowScroll: true,
+ selectOnBlur: true,
+ timeFormat: 'H:i',
+ });
+
+ $this.addClass('validator');
+ $this.addClass('time-validator');
+ $this.each(function () {
+ var oldValidate = this.validate;
+ this.validate = function () {
+ if (oldValidate) {
+ oldValidate.call(this);
+ }
+ validate();
+ }
+ });
});
-
+
makeStrippedInput($input);
}
function makeUrlValidator($input) {
- if (!$input.length) {
- return;
- }
-
- function isValid(strVal) {
- if (!$input.is(':visible')) {
- return true; /* an invisible element is considered always valid */
- }
+ $input.each(function () {
+ var $this = $(this);
- return strVal.match(new RegExp('^([a-zA-Z]+)://([\\w\-.]+)(:\\d+)?(/.*)?$')) != null;
- }
-
- var msg = 'enter a valid URL (e.g. http://example.com:8080/cams/)';
+ function isValid(strVal) {
+ if (!$this.is(':visible')) {
+ return true; /* an invisible element is considered always valid */
+ }
- function validate() {
- var strVal = $input.val();
- if (isValid(strVal)) {
- $input.attr('title', '');
- $input.removeClass('error');
- $input[0].invalid = false;
+ return strVal.match(new RegExp('^([a-zA-Z]+)://([\\w\-.]+)(:\\d+)?(/.*)?$')) != null;
}
- else {
- $input.attr('title', msg);
- $input.addClass('error');
- $input[0].invalid = true;
- }
- }
-
- $input.keyup(validate);
- $input.blur(validate);
- $input.change(validate).change();
-
- $input.addClass('validator');
- $input.addClass('url-validator');
- $input.each(function () {
- var oldValidate = this.validate;
- this.validate = function () {
- if (oldValidate) {
- oldValidate.call(this);
+
+ var msg = 'enter a valid URL (e.g. http://example.com:8080/cams/)';
+
+ function validate() {
+ var strVal = $this.val();
+ if (isValid(strVal)) {
+ $this.attr('title', '');
+ $this.removeClass('error');
+ $this[0].invalid = false;
+ }
+ else {
+ $this.attr('title', msg);
+ $this.addClass('error');
+ $this[0].invalid = true;
}
- validate();
}
+
+ $this.keyup(validate);
+ $this.blur(validate);
+ $this.change(validate).change();
+
+ $this.addClass('validator');
+ $this.addClass('url-validator');
+ $this.each(function () {
+ var oldValidate = this.validate;
+ this.validate = function () {
+ if (oldValidate) {
+ oldValidate.call(this);
+ }
+ validate();
+ }
+ });
});
}
function makeCustomValidator($input, isValidFunc) {
- if (!$input.length) {
- return;
- }
-
- function isValid(strVal) {
- if (!$input.is(':visible')) {
- return true; /* an invisible element is considered always valid */
- }
+ $input.each(function () {
+ var $this = $(this);
- return isValidFunc(strVal);
- }
-
- function validate() {
- var strVal = $input.val();
- var valid = isValid(strVal);
- if (valid == true) {
- $input.attr('title', '');
- $input.removeClass('error');
- $input[0].invalid = false;
+ function isValid(strVal) {
+ if (!$this.is(':visible')) {
+ return true; /* an invisible element is considered always valid */
+ }
+
+ return isValidFunc(strVal);
}
- else {
- $input.attr('title', valid || 'enter a valid value');
- $input.addClass('error');
- $input[0].invalid = true;
+
+ function validate() {
+ var strVal = $this.val();
+ var valid = isValid(strVal);
+ if (valid == true) {
+ $this.attr('title', '');
+ $this.removeClass('error');
+ $this[0].invalid = false;
+ }
+ else {
+ $this.attr('title', valid || 'enter a valid value');
+ $this.addClass('error');
+ $this[0].invalid = true;
+ }
}
- }
-
- $input.keyup(validate);
- $input.blur(validate);
- $input.change(validate).change();
- $input.addClass('validator');
- $input.addClass('custom-validator');
- $input.each(function () {
- var oldValidate = this.validate;
- this.validate = function () {
- if (oldValidate) {
- oldValidate.call(this);
+ $this.keyup(validate);
+ $this.blur(validate);
+ $this.change(validate).change();
+
+ $this.addClass('validator');
+ $this.addClass('custom-validator');
+ $this.each(function () {
+ var oldValidate = this.validate;
+ this.validate = function () {
+ if (oldValidate) {
+ oldValidate.call(this);
+ }
+ validate();
}
- validate();
- }
+ });
});
}
{% extends "base.html" %}
+{% macro config_item(config) -%}
+ <tr class="settings-item additional-config {% if config['advanced'] %}advanced-setting{% endif %}"
+ {% if config.get('reboot') %}reboot="true"{% endif %}
+ {% if config.get('required') %}required="true"{% endif %}
+ {% if config.get('strip') %}strip="true"{% endif %}
+ {% if config.get('depends') %}depends="{{' '.join(config['depends'])}}"{% endif %}
+ {% if config.get('min') is not none %}min="{{config['min']}}"{% endif %}
+ {% if config.get('max') is not none %}max="{{config['min']}}"{% endif %}
+ {% if config.get('floating') %}floating="true"{% endif %}
+ {% if config.get('sign') %}sign="true"{% endif %}
+ {% if config.get('snap') is not none %}snap="{{config['snap']}}"{% endif %}
+ {% if config.get('ticks') %}ticks="{{config['ticks']}}"{% endif %}
+ {% if config.get('ticksnum') is not none %}ticksnum="{{config['ticksnum']}}"{% endif %}
+ {% if config.get('decimals') is not none %}decimals="{{config['decimals']}}"{% endif %}
+ {% if config.get('unit') %}unit="{{config['unit']}}"{% endif %}>
+ <td class="settings-item-label"><span class="settings-item-label">{{config['label']}}</span></td>
+ <td class="settings-item-value">
+ {% if config['type'] == 'str' %}
+ <input type="text" class="styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Entry">
+ {% elif config['type'] == 'pwd' %}
+ <input type="password" class="styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Entry">
+ {% elif config['type'] == 'number' %}
+ <input type="text" class="number styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Entry">
+ {% elif config['type'] == 'range' %}
+ <input type="text" class="range styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Slider">
+ {% elif config['type'] == 'bool' %}
+ <input type="checkbox" class="styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Switch">
+ {% elif config['type'] == 'choices' %}
+ <select class="styled {{config['section']}} {% if config.get('camera') %}camera{% else %}main{% endif %}-config" id="{{config['name']}}Select">
+ {% for choice in config['choices'] %}
+ <option value="{{choice[0]}}">{{choice[1]}}</option>
+ {% endfor %}
+ </select>
+ {% endif %}
+ </td>
+ <td>{% if config.get('description') %}<span class="help-mark" title="{{config['description']}}">?</span>{% endif %}</td>
+ </tr>
+{%- endmacro %}
+
{% block title %}{% if title %}{{title}}{% else %}motionEye{% endif %}{% endblock %}
{% block style %}
<div class="page">
<div class="settings closed">
<div class="settings-container">
- <div class="settings-section-title"><input type="checkbox" class="styled section general" id="motionEyeSwitch">General Settings</div>
+ <div class="settings-section-title"><input type="checkbox" class="styled section general main-config" id="motionEyeSwitch">General Settings</div>
<table class="settings">
<tr class="settings-item">
<td class="settings-item-label"><span class="settings-item-label">Show Advanced Settings</span></td>
- <td class="settings-item-value"><input type="checkbox" class="styled general" id="showAdvancedSwitch"></td>
+ <td class="settings-item-value"><input type="checkbox" class="styled general main-config" id="showAdvancedSwitch"></td>
<td><span class="help-mark" title="enable this to be able to access all the advanced settings">?</span></td>
</tr>
- <tr class="settings-item">
+ <tr class="settings-item" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Administrator Username</span></td>
- <td class="settings-item-value"><input type="text" class="styled general" id="adminUsernameEntry" readonly="readonly"></td>
+ <td class="settings-item-value"><input type="text" class="styled general main-config" id="adminUsernameEntry" readonly="readonly"></td>
<td><span class="help-mark" title="the username supplied to configure motionEye">?</span></td>
</tr>
- <tr class="settings-item">
+ <tr class="settings-item" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Administrator Password</span></td>
- <td class="settings-item-value"><input type="password" class="styled general" id="adminPasswordEntry"></td>
+ <td class="settings-item-value"><input type="password" class="styled general main-config" id="adminPasswordEntry"></td>
<td><span class="help-mark" title="administrator's password">?</span></td>
</tr>
- <tr class="settings-item">
+ <tr class="settings-item" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Surveillance Username</span></td>
- <td class="settings-item-value"><input type="text" class="styled general" id="normalUsernameEntry"></td>
+ <td class="settings-item-value"><input type="text" class="styled general main-config" id="normalUsernameEntry"></td>
<td><span class="help-mark" title="the username supplied for video surveillance">?</span></td>
</tr>
- <tr class="settings-item">
+ <tr class="settings-item" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Surveillance Password</span></td>
- <td class="settings-item-value"><input type="password" class="styled general" id="normalPasswordEntry"></td>
+ <td class="settings-item-value"><input type="password" class="styled general main-config" id="normalPasswordEntry"></td>
<td><span class="help-mark" title="the password for the surveillance user (leave empty for passwordless surveillance)">?</span></td>
</tr>
- <tr class="settings-item advanced-setting{% if not timezones %} hidden{% endif %}">
- <td class="settings-item-label"><span class="settings-item-label">Time Zone</span></td>
- <td class="settings-item-value">
- <select class="styled general" id="timeZoneSelect">
- {% for timezone in timezones %}
- <option value="{{timezone}}">{{timezone}}</option>
- {% endfor %}
- </select>
- </td>
- <td><span class="help-mark" title="selecting the right timezone assures a correct timestamp displayed on pictures and movies">?</span></td>
- </tr>
+ {% for config in main_sections.get('general', {}).get('configs', []) %}
+ {{config_item(config)}}
+ {% endfor %}
<tr class="settings-item advanced-setting">
<td colspan="100"><div class="settings-item-separator"></div></td>
</tr>
</tr>
</table>
- <div class="settings-section-title advanced-setting{% if not wpa_supplicant %} hidden{% endif %}"><input type="checkbox" class="styled section wifi" id="wifiSwitch">Wireless Network</div>
- <table class="settings advanced-setting{% if not wpa_supplicant %} hidden{% endif %}">
- <tr class="settings-item advanced-setting">
- <td class="settings-item-label"><span class="settings-item-label">Network Name</span></td>
- <td class="settings-item-value"><input type="text" class="styled wifi" id="wifiNameEntry" placeholder="name..."></td>
- <td><span class="help-mark" title="the name (SSID) of your wireless network">?</span></td>
- </tr>
- <tr class="settings-item advanced-setting">
- <td class="settings-item-label"><span class="settings-item-label">Network Key</span></td>
- <td class="settings-item-value"><input type="password" class="styled wifi" id="wifiKeyEntry" placeholder="key..."></td>
- <td><span class="help-mark" title="the key (PSK) required to connect to your wireless network">?</span></td>
- </tr>
+ {% for section in main_sections.values() %}
+ {% if section.get('label') and section.get('configs') %}
+ <div class="settings-section-title {% if section['advanced'] %}advanced-setting{% endif %}" title="{{section.get('description') or ''}}">{% if section.get('onoff') %}
+ <input type="checkbox" class="styled section additional-section {{section['name']}} main-config" id="{{section['name']}}Switch">{% endif %}{{section['label']}}</div>
+ <table class="settings">
+ {% for config in section['configs'] %}
+ {{config_item(config)}}
+ {% endfor %}
</table>
+ {% endif %}
+ {% endfor %}
- <div class="settings-section-title"><input type="checkbox" class="styled section device" id="videoDeviceSwitch">Video Device</div>
+ <div class="settings-section-title"><input type="checkbox" class="styled section device camera-config" id="videoDeviceSwitch">Video Device</div>
<table class="settings">
- <tr class="settings-item">
+ <tr class="settings-item" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Camera Name</span></td>
- <td class="settings-item-value"><input type="text" class="styled device" id="deviceNameEntry" placeholder="camera name..."></td>
+ <td class="settings-item-value"><input type="text" class="styled device camera-config" id="deviceNameEntry" placeholder="camera name..."></td>
<td><span class="help-mark" title="an alias for this camera device">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Camera Device</span></td>
- <td class="settings-item-value"><input type="text" class="styled device" id="deviceEntry" disabled="disabled"></td>
+ <td class="settings-item-value"><input type="text" class="styled device camera-config" id="deviceEntry" disabled="disabled"></td>
</tr>
<tr class="settings-item advanced-setting">
<td colspan="100"><div class="settings-item-separator"></div></td>
</tr>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Light Switch Detection</span></td>
- <td class="settings-item-value"><input type="checkbox" class="styled device" id="lightSwitchDetectSwitch"></td>
+ <td class="settings-item-value"><input type="checkbox" class="styled device camera-config" id="lightSwitchDetectSwitch"></td>
<td><span class="help-mark" title="enable this if you want sudden changes in light to not be treated as motion">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Automatic Brightness</span></td>
- <td class="settings-item-value"><input type="checkbox" class="styled device" id="autoBrightnessSwitch"></td>
+ <td class="settings-item-value"><input type="checkbox" class="styled device camera-config" id="autoBrightnessSwitch"></td>
<td><span class="help-mark" title="enables software automatic brightness (only recommended for cameras without autobrightness)">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
<td class="settings-item-label"><span class="settings-item-label">Brightness</span></td>
- <td class="settings-item-value"><input type="text" class="range styled device" id="brightnessSlider"></td>
+ <td class="settings-item-value"><input type="text" class="range styled device camera-config" id="brightnessSlider"></td>
<td><span class="help-mark" title="sets a desired brightness level for this camera">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
<td class="settings-item-label"><span class="settings-item-label">Contrast</span></td>
- <td class="settings-item-value"><input type="text" class="range styled device" id="contrastSlider"></td>
+ <td class="settings-item-value"><input type="text" class="range styled device camera-config" id="contrastSlider"></td>
<td><span class="help-mark" title="sets a desired contrast level for this camera">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
<td class="settings-item-label"><span class="settings-item-label">Saturation</span></td>
- <td class="settings-item-value"><input type="text" class="range styled device" id="saturationSlider"></td>
+ <td class="settings-item-value"><input type="text" class="range styled device camera-config" id="saturationSlider"></td>
<td><span class="help-mark" title="sets a desired saturation (color) level for this camera">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
<td class="settings-item-label"><span class="settings-item-label">Hue</span></td>
- <td class="settings-item-value"><input type="text" class="range styled device" id="hueSlider"></td>
+ <td class="settings-item-value"><input type="text" class="range styled device camera-config" id="hueSlider"></td>
<td><span class="help-mark" title="sets a desired hue (color) for this camera">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Video Resolution</span></td>
<td class="settings-item-value">
- <select class="video-resolution styled device" id="resolutionSelect">
+ <select class="video-resolution styled device camera-config" id="resolutionSelect">
</select>
</td>
<td><span class="help-mark" title="the video resolution (larger values produce better quality but require larger storage space and bandwidth)">?</span></td>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Video Rotation</span></td>
<td class="settings-item-value">
- <select class="rotation styled device" id="rotationSelect">
+ <select class="rotation styled device camera-config" id="rotationSelect">
<option value="0">0°</option>
<option value="90">90°</option>
<option value="180">180°</option>
</td>
<td><span class="help-mark" title="use this to rotate the captured image, if your camera is not positioned correctly">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="2" max="30" snap="0" ticks="2|5|10|15|20|25|30" decimals="0">
<td class="settings-item-label"><span class="settings-item-label">Frame Rate</span></td>
- <td class="settings-item-value"><input type="text" class="range styled device" id="framerateSlider"></td>
+ <td class="settings-item-value"><input type="text" class="range styled device camera-config" id="framerateSlider"></td>
<td><span class="help-mark" title="sets the number of frames captured by the camera every second (higher values produce smoother videos but require larger storage space and bandwidth)">?</span></td>
</tr>
+ {% for config in camera_sections.get('device', {}).get('configs', []) %}
+ {{config_item(config)}}
+ {% endfor %}
</table>
<div class="settings-section-title advanced-setting">File Storage</div>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Storage Device</span></td>
<td class="settings-item-value">
- <select class="styled storage" id="storageDeviceSelect">
+ <select class="styled storage camera-config" id="storageDeviceSelect">
</select>
</td>
<td><span class="help-mark" title="indicates the storage device where the image and video files will be saved">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Network Server</span></td>
- <td class="settings-item-value"><input type="text" class="styled storage" id="networkServerEntry"></td>
+ <td class="settings-item-value"><input type="text" class="styled storage camera-config" id="networkServerEntry"></td>
<td><span class="help-mark" title="the address of the network server (IP address or hostname)">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Share Name</span></td>
- <td class="settings-item-value"><input type="text" class="styled storage" id="networkShareNameEntry"></td>
+ <td class="settings-item-value"><input type="text" class="styled storage camera-config" id="networkShareNameEntry"></td>
<td><span class="help-mark" title="the name of the network share">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Share Username</span></td>
- <td class="settings-item-value"><input type="text" class="styled storage" id="networkUsernameEntry"></td>
+ <td class="settings-item-value"><input type="text" class="styled storage camera-config" id="networkUsernameEntry"></td>
<td><span class="help-mark" title="the username to be supplied when accessing the network share (leave empty if no username is required)">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Share Password</span></td>
- <td class="settings-item-value"><input type="password" class="styled storage" id="networkPasswordEntry"></td>
+ <td class="settings-item-value"><input type="password" class="styled storage camera-config" id="networkPasswordEntry"></td>
<td><span class="help-mark" title="the password required by the network share (leave empty if no password is required)">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Root Directory</span></td>
- <td class="settings-item-value"><input type="text" class="styled storage" id="rootDirectoryEntry"></td>
+ <td class="settings-item-value"><input type="text" class="styled storage camera-config" id="rootDirectoryEntry"></td>
<td><span class="help-mark" title="the root path (on the selected storage device) where the files will be saved">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Disk Usage</span></td>
<td class="settings-item-value">
- <div id="diskUsageProgressBar"></div>
+ <div id="diskUsageProgressBar" class="progress-bar"></div>
</td>
<td><span class="help-mark" title="the used/total size of the disk where the root directory resides">?</span></td>
</tr>
+ {% for config in camera_sections.get('storage', {}).get('configs', []) %}
+ {{config_item(config)}}
+ {% endfor %}
</table>
- <div class="settings-section-title advanced-setting"><input type="checkbox" class="styled section text-overlay" id="textOverlaySwitch">Text Overlay</div>
+ <div class="settings-section-title advanced-setting"><input type="checkbox" class="styled section text-overlay camera-config" id="textOverlaySwitch">Text Overlay</div>
<table class="settings advanced-setting">
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Left Text</span></td>
<td class="settings-item-value">
- <select class="styled text-overlay" id="leftTextSelect">
+ <select class="styled text-overlay camera-config" id="leftTextSelect">
<option value="camera-name">Camera Name</option>
<option value="timestamp">Timestamp</option>
<option value="custom-text">Custom Text</option>
</td>
<td><span class="help-mark" title="sets the text displayed on the movies and images, on the lower left corner">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"></td>
- <td class="settings-item-value"><input type="text" class="styled text-overlay" id="leftTextEntry" placeholder="custom text..."></td>
+ <td class="settings-item-value"><input type="text" class="styled text-overlay camera-config" id="leftTextEntry" placeholder="custom text..."></td>
<td><span class="help-mark" title="sets a custom left text; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %T = HH:MM:SS, %q = frame number, \n = new line">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Right Text</span></td>
<td class="settings-item-value">
- <select class="styled text-overlay" id="rightTextSelect">
+ <select class="styled text-overlay camera-config" id="rightTextSelect">
<option value="camera-name">Camera Name</option>
<option value="timestamp">Timestamp</option>
<option value="custom-text">Custom Text</option>
</td>
<td><span class="help-mark" title="sets the text displayed on the movies and images, on the lower right corner">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"></td>
- <td class="settings-item-value"><input type="text" class="styled text-overlay" id="rightTextEntry" placeholder="custom text..."></td>
+ <td class="settings-item-value"><input type="text" class="styled text-overlay camera-config" id="rightTextEntry" placeholder="custom text..."></td>
<td><span class="help-mark" title="sets a custom right text; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %T = HH:MM:SS, %q = frame number, \n = new line">?</span></td>
</tr>
+ {% for config in camera_sections.get('text-overlay', {}).get('configs', []) %}
+ {{config_item(config)}}
+ {% endfor %}
</table>
- <div class="settings-section-title"><input type="checkbox" class="styled section streaming" id="videoStreamingSwitch">Video Streaming</div>
+ <div class="settings-section-title"><input type="checkbox" class="styled section streaming camera-config" id="videoStreamingSwitch">Video Streaming</div>
<table class="settings">
- <tr class="settings-item advanced-setting local-streaming">
+ <tr class="settings-item advanced-setting local-streaming" min="1" max="30" snap="0" ticks="1|5|10|15|20|25|30" decimals="0">
<td class="settings-item-label"><span class="settings-item-label">Streaming Frame Rate</span></td>
- <td class="settings-item-value"><input type="text" class="range styled streaming" id="streamingFramerateSlider"></td>
+ <td class="settings-item-value"><input type="text" class="range styled streaming camera-config" id="streamingFramerateSlider"></td>
<td><span class="help-mark" title="sets the number of frames transmitted every second on the live streaming">?</span></td>
</tr>
- <tr class="settings-item advanced-setting local-streaming">
+ <tr class="settings-item advanced-setting local-streaming" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
<td class="settings-item-label"><span class="settings-item-label">Streaming Quality</span></td>
- <td class="settings-item-value"><input type="text" class="range styled streaming" id="streamingQualitySlider"></td>
+ <td class="settings-item-value"><input type="text" class="range styled streaming camera-config" id="streamingQualitySlider"></td>
<td><span class="help-mark" title="sets the live streaming quality (higher values produce a better video quality but require more bandwidth)">?</span></td>
</tr>
<tr class="settings-item advanced-setting local-streaming">
<td class="settings-item-label"><span class="settings-item-label">Streaming Image Resizing</span></td>
- <td class="settings-item-value"><input type="checkbox" class="styled streaming" id="streamingServerResizeSwitch"></td>
+ <td class="settings-item-value"><input type="checkbox" class="styled streaming camera-config" id="streamingServerResizeSwitch"></td>
<td><span class="help-mark" title="when this is enabled, the images are resized by motionEye before they are sent to the browser (disable when running motionEye on a slow CPU)">?</span></td>
</tr>
- <tr class="settings-item advanced-setting local-streaming">
+ <tr class="settings-item advanced-setting local-streaming" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
<td class="settings-item-label"><span class="settings-item-label">Streaming Resolution</span></td>
- <td class="settings-item-value"><input type="text" class="range styled streaming" id="streamingResolutionSlider"></td>
+ <td class="settings-item-value"><input type="text" class="range styled streaming camera-config" id="streamingResolutionSlider"></td>
<td><span class="help-mark" title="the streaming resolution given as percent of the video device resolution (higher values produce better video quality but require more bandwidth)">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="1024" max="65535" required="true">
<td class="settings-item-label"><span class="settings-item-label">Streaming Port</span></td>
- <td class="settings-item-value"><input type="text" class="styled streaming" id="streamingPortEntry"></td>
+ <td class="settings-item-value"><input type="text" class="styled streaming camera-config" id="streamingPortEntry"></td>
<td><span class="help-mark" title="sets the TCP port on which the webcam streaming server listens">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Motion Optimization</span></td>
- <td class="settings-item-value"><input type="checkbox" class="styled streaming" id="streamingMotion"></td>
+ <td class="settings-item-value"><input type="checkbox" class="styled streaming camera-config" id="streamingMotion"></td>
<td><span class="help-mark" title="enable this if you want a lower frame rate for the live streaming when no motion is detected">?</span></td>
</tr>
<tr class="settings-item advanced-setting local-streaming">
<td class="settings-item-label"><span class="settings-item-label">Snapshot URL</span></td>
- <td class="settings-item-value"><input type="text" class="styled streaming" id="streamingSnapshotUrlEntry" readonly="readonly"></td>
+ <td class="settings-item-value"><input type="text" class="styled streaming camera-config" id="streamingSnapshotUrlEntry" readonly="readonly"></td>
<td><span class="help-mark" title="a URL that provides a JPEG image with the most recent snapshot of the camera">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Streaming URL</span></td>
- <td class="settings-item-value"><input type="text" class="styled streaming" id="streamingMjpgUrlEntry" readonly="readonly"></td>
+ <td class="settings-item-value"><input type="text" class="styled streaming camera-config" id="streamingMjpgUrlEntry" readonly="readonly"></td>
<td><span class="help-mark" title="a URL that provides a MJPEG stream of the camera (there is no password protection for this URL!)">?</span></td>
</tr>
<tr class="settings-item advanced-setting local-streaming">
<td class="settings-item-label"><span class="settings-item-label">Embed URL</span></td>
- <td class="settings-item-value"><input type="text" class="styled streaming" id="streamingEmbedUrlEntry" readonly="readonly"></td>
+ <td class="settings-item-value"><input type="text" class="styled streaming camera-config" id="streamingEmbedUrlEntry" readonly="readonly"></td>
<td><span class="help-mark" title="a URL that provides a minimal HTML document containing the camera frame, ready to be embedded">?</span></td>
</tr>
+ {% for config in camera_sections.get('streaming', {}).get('configs', []) %}
+ {{config_item(config)}}
+ {% endfor %}
</table>
- <div class="settings-section-title"><input type="checkbox" class="styled section still-images" id="stillImagesSwitch">Still Images</div>
+ <div class="settings-section-title"><input type="checkbox" class="styled section still-images camera-config" id="stillImagesSwitch">Still Images</div>
<table class="settings">
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Image File Name</span></td>
- <td class="settings-item-value"><input type="text" class="styled still-images" id="imageFileNameEntry" placeholder="file name pattern..."></td>
+ <td class="settings-item-value"><input type="text" class="styled still-images camera-config" id="imageFileNameEntry" placeholder="file name pattern..."></td>
<td><span class="help-mark" title="sets the name pattern for the image (JPEG) files; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %q = frame number, %v = event number / = subfolder">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
<td class="settings-item-label"><span class="settings-item-label">Image Quality</span></td>
- <td class="settings-item-value"><input type="text" class="range styled still-images" id="imageQualitySlider"></td>
+ <td class="settings-item-value"><input type="text" class="range styled still-images camera-config" id="imageQualitySlider"></td>
<td><span class="help-mark" title="sets the JPEG image quality (higher values produce a better image quality but require more storage space)">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Capture Mode</span></td>
<td class="settings-item-value">
- <select class="styled still-images" id="captureModeSelect">
+ <select class="styled still-images camera-config" id="captureModeSelect">
<option value="motion-triggered">Motion Triggered</option>
<option value="interval-snapshots">Interval Snapshots</option>
<option value="all-frames">All Frames</option>
</td>
<td><span class="help-mark" title="sets the image capture mode: Motion Triggered = an image captured whenever motion is detected, Automated Snapshots = an image captured every x seconds, All Frames = saves each frame into an image file">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="1" max="86400" required="true">
<td class="settings-item-label"><span class="settings-item-label">Snapshot Interval</span></td>
- <td class="settings-item-value"><input type="text" class="styled number still-images" id="snapshotIntervalEntry"><span class="settings-item-unit">seconds</span></td>
+ <td class="settings-item-value"><input type="text" class="styled number still-images camera-config" id="snapshotIntervalEntry"><span class="settings-item-unit">seconds</span></td>
<td><span class="help-mark" title="sets the interval (in seconds) for the automated snapshots">?</span></td>
</tr>
<tr class="settings-item">
<td class="settings-item-label"><span class="settings-item-label">Preserve Pictures</span></td>
<td class="settings-item-value">
- <select class="styled still-images" id="preservePicturesSelect">
+ <select class="styled still-images camera-config" id="preservePicturesSelect">
<option value="1">For One Day</option>
<option value="7">For One Week</option>
<option value="30">For One Month</option>
</td>
<td><span class="help-mark" title="images older than the specified duration are automatically deleted to free storage space">?</span></td>
</tr>
- <tr class="settings-item">
+ <tr class="settings-item" min="1" max="3650" required="true">
<td class="settings-item-label"><span class="settings-item-label">Pictures Lifetime</span></td>
- <td class="settings-item-value"><input type="text" class="styled number still-images" id="picturesLifetimeEntry"><span class="settings-item-unit">days</span></td>
+ <td class="settings-item-value"><input type="text" class="styled number still-images camera-config" id="picturesLifetimeEntry"><span class="settings-item-unit">days</span></td>
<td><span class="help-mark" title="sets the number of days after which the pictures will be deleted automatically">?</span></td>
</tr>
+ {% for config in camera_sections.get('still-images', {}).get('configs', []) %}
+ {{config_item(config)}}
+ {% endfor %}
</table>
- <div class="settings-section-title advanced-setting"><input type="checkbox" class="styled section motion-detection" id="motionDetectionSwitch">Motion Detection</div>
+ <div class="settings-section-title advanced-setting"><input type="checkbox" class="styled section motion-detection camera-config" id="motionDetectionSwitch">Motion Detection</div>
<table class="settings advanced-setting">
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Show Frame Changes</span></td>
- <td class="settings-item-value"><input type="checkbox" class="styled motion-detection" id="showFrameChangesSwitch"></td>
+ <td class="settings-item-value"><input type="checkbox" class="styled motion-detection camera-config" id="showFrameChangesSwitch"></td>
<td><span class="help-mark" title="if this is enabled, frame changes (number of pixels as well as the changed area) are shown on the video; temporarily enable this option to help adjust the motion detection parameters">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="0" max="20" snap="0" ticksnum="5" decimals="1" unit="%">
<td class="settings-item-label"><span class="settings-item-label">Frame Change Threshold</span></td>
- <td class="settings-item-value"><input type="text" class="range styled motion-detection" id="frameChangeThresholdSlider"></td>
+ <td class="settings-item-value"><input type="text" class="range styled motion-detection camera-config" id="frameChangeThresholdSlider"></td>
<td><span class="help-mark" title="indicates the minimal percent of the image that must change between two successive frames in order for motion to be detected (smaller values give a more sensitive detection, but are prone to false positives)">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Auto Noise Detection</span></td>
- <td class="settings-item-value"><input type="checkbox" class="styled motion-detection" id="autoNoiseDetectSwitch"></td>
+ <td class="settings-item-value"><input type="checkbox" class="styled motion-detection camera-config" id="autoNoiseDetectSwitch"></td>
<td><span class="help-mark" title="enable this to automatically adjust the noise level">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="0" max="25" snap="0" ticksnum="6" decimals="0" unit="%">
<td class="settings-item-label"><span class="settings-item-label">Noise Level</span></td>
- <td class="settings-item-value"><input type="text" class="range styled motion-detection" id="noiseLevelSlider"></td>
+ <td class="settings-item-value"><input type="text" class="range styled motion-detection camera-config" id="noiseLevelSlider"></td>
<td><span class="help-mark" title="manually sets the noise level to a fixed value">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
<td colspan="100"><div class="settings-item-separator"></div></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="1" max="86400" required="true">
<td class="settings-item-label"><span class="settings-item-label">Motion Gap</span></td>
- <td class="settings-item-value"><input type="text" class="number styled motion-detection" id="eventGapEntry"><span class="settings-item-unit">seconds</span></td>
+ <td class="settings-item-value"><input type="text" class="number styled motion-detection camera-config" id="eventGapEntry"><span class="settings-item-unit">seconds</span></td>
<td><span class="help-mark" title="sets the number of seconds of silence (i.e. no motion) that mark the end of a motion event">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="0" max="100" required="true">
<td class="settings-item-label"><span class="settings-item-label">Captured Before</span></td>
- <td class="settings-item-value"><input type="text" class="number styled motion-detection" id="preCaptureEntry"><span class="settings-item-unit">frames</span></td>
+ <td class="settings-item-value"><input type="text" class="number styled motion-detection camera-config" id="preCaptureEntry"><span class="settings-item-unit">frames</span></td>
<td><span class="help-mark" title="sets the number of frames to be captured (and included in the movie) before a motion event is detected">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="0" max="100" required="true">
<td class="settings-item-label"><span class="settings-item-label">Captured After</span></td>
- <td class="settings-item-value"><input type="text" class="number styled motion-detection" id="postCaptureEntry"><span class="settings-item-unit">frames</span></td>
+ <td class="settings-item-value"><input type="text" class="number styled motion-detection camera-config" id="postCaptureEntry"><span class="settings-item-unit">frames</span></td>
<td><span class="help-mark" title="sets the number of frames to be captured (and included in the movie) after a motion event is detected">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="1" max="1000" required="true">
<td class="settings-item-label"><span class="settings-item-label">Minimum Motion Frames</span></td>
- <td class="settings-item-value"><input type="text" class="number styled motion-detection" id="minimumMotionFramesEntry"><span class="settings-item-unit">frames</span></td>
+ <td class="settings-item-value"><input type="text" class="number styled motion-detection camera-config" id="minimumMotionFramesEntry"><span class="settings-item-unit">frames</span></td>
<td><span class="help-mark" title="sets the minimum number of successive motion frames required to start a motion event">?</span></td>
</tr>
+ {% for config in camera_sections.get('motion-detection', {}).get('configs', []) %}
+ {{config_item(config)}}
+ {% endfor %}
</table>
- <div class="settings-section-title"><input type="checkbox" class="styled section motion-movies" id="motionMoviesSwitch">Motion Movies</div>
+ <div class="settings-section-title"><input type="checkbox" class="styled section motion-movies camera-config" id="motionMoviesSwitch">Motion Movies</div>
<table class="settings">
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Movie File Name</span></td>
- <td class="settings-item-value"><input type="text" class="styled motion-movies" id="movieFileNameEntry" placeholder="file name pattern..."></td>
+ <td class="settings-item-value"><input type="text" class="styled motion-movies camera-config" id="movieFileNameEntry" placeholder="file name pattern..."></td>
<td><span class="help-mark" title="sets the name pattern for the movie (MPEG) files; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %q = frame number, / = subfolder">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="0" max="100" snap="2" ticksnum="5" decimals="0" unit="%">
<td class="settings-item-label"><span class="settings-item-label">Movie Quality</span></td>
- <td class="settings-item-value"><input type="text" class="range styled motion-movies" id="movieQualitySlider"></td>
+ <td class="settings-item-value"><input type="text" class="range styled motion-movies camera-config" id="movieQualitySlider"></td>
<td><span class="help-mark" title="sets the MPEG video quality (higher values produce a better video quality but require more storage space)">?</span></td>
</tr>
<tr class="settings-item">
<td class="settings-item-label"><span class="settings-item-label">Preserve Movies</span></td>
<td class="settings-item-value">
- <select class="styled motion-movies" id="preserveMoviesSelect">
+ <select class="styled motion-movies camera-config" id="preserveMoviesSelect">
<option value="1">For One Day</option>
<option value="7">For One Week</option>
<option value="30">For One Month</option>
</td>
<td><span class="help-mark" title="movies older than the specified duration are automatically deleted to free storage space">?</span></td>
</tr>
- <tr class="settings-item">
+ <tr class="settings-item" min="1" max="3650" required="true">
<td class="settings-item-label"><span class="settings-item-label">Movies Lifetime</span></td>
- <td class="settings-item-value"><input type="text" class="styled number still-images" id="moviesLifetimeEntry"><span class="settings-item-unit">days</span></td>
+ <td class="settings-item-value"><input type="text" class="styled number motion-movies camera-config" id="moviesLifetimeEntry"><span class="settings-item-unit">days</span></td>
<td><span class="help-mark" title="sets the number of days after which the movies will be deleted automatically">?</span></td>
</tr>
+ {% for config in camera_sections.get('motion-movies', {}).get('configs', []) %}
+ {{config_item(config)}}
+ {% endfor %}
</table>
<div class="settings-section-title advanced-setting">Motion Notifications</div>
<table class="settings">
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Email Notifications</span></td>
- <td class="settings-item-value"><input type="checkbox" class="styled notifications" id="emailNotificationsSwitch"></td>
+ <td class="settings-item-value"><input type="checkbox" class="styled notifications camera-config" id="emailNotificationsSwitch"></td>
<td><span class="help-mark" title="enable this if you want to receive email notifications whenever a motion event is detected">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Email Addresses</span></td>
- <td class="settings-item-value"><input type="text" class="styled notifications" id="emailAddressesEntry" placeholder="email addresses..."></td>
+ <td class="settings-item-value"><input type="text" class="styled notifications camera-config" id="emailAddressesEntry" placeholder="email addresses..."></td>
<td><span class="help-mark" title="email addresses (separated by comma) that are added here will receive notifications whenever a motion event is detected">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">SMTP Server</span></td>
- <td class="settings-item-value"><input type="text" class="styled notifications" id="smtpServerEntry" placeholder="e.g. smtp.gmail.com"></td>
+ <td class="settings-item-value"><input type="text" class="styled notifications camera-config" id="smtpServerEntry" placeholder="e.g. smtp.gmail.com"></td>
<td><span class="help-mark" title="enter the hostname or IP address of your SMTP server (for Gmail use smtp.gmail.com)">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="1" max="65535" required="true">
<td class="settings-item-label"><span class="settings-item-label">SMTP Port</span></td>
- <td class="settings-item-value"><input type="text" class="styled number notifications" id="smtpPortEntry" placeholder="e.g. 587"></td>
+ <td class="settings-item-value"><input type="text" class="styled number notifications camera-config" id="smtpPortEntry" placeholder="e.g. 587"></td>
<td><span class="help-mark" title="enter the port used by your SMTP server (usually 465 for non-TLS connections and 587 for TLS connections)">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">SMTP Account</span></td>
- <td class="settings-item-value"><input type="text" class="styled notifications" id="smtpAccountEntry" placeholder="account@gmail.com..."></td>
+ <td class="settings-item-value"><input type="text" class="styled notifications camera-config" id="smtpAccountEntry" placeholder="account@gmail.com..."></td>
<td><span class="help-mark" title="enter your SMTP account (normally your email address)">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">SMTP Password</span></td>
- <td class="settings-item-value"><input type="password" class="styled notifications" id="smtpPasswordEntry"></td>
+ <td class="settings-item-value"><input type="password" class="styled notifications camera-config" id="smtpPasswordEntry"></td>
<td><span class="help-mark" title="enter your SMTP account password (for Gmail use your Google password or an app-specific generated password)">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Use TLS</span></td>
- <td class="settings-item-value"><input type="checkbox" class="styled notifications" id="smtpTlsSwitch"></td>
+ <td class="settings-item-value"><input type="checkbox" class="styled notifications camera-config" id="smtpTlsSwitch"></td>
<td><span class="help-mark" title="enable this if your SMTP server requires TLS (Gmail needs this to be enabled)">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" min="0" max="60" required="true">
<td class="settings-item-label"><span class="settings-item-label">Attached Pictures Time Span</span></td>
- <td class="settings-item-value"><input type="text" class="number styled notifications" id="emailPictureTimeSpanEntry"><span class="settings-item-unit">seconds</span></td>
+ <td class="settings-item-value"><input type="text" class="number styled notifications camera-config" id="emailPictureTimeSpanEntry"><span class="settings-item-unit">seconds</span></td>
<td><span class="help-mark" title="defines the picture search time interval to use when creating email attachments (higher values generate emails with more pictures at the cost of an increased notification delay)">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
</tr>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Web Hook Notifications</span></td>
- <td class="settings-item-value"><input type="checkbox" class="styled notifications" id="webHookNotificationsSwitch"></td>
+ <td class="settings-item-value"><input type="checkbox" class="styled notifications camera-config" id="webHookNotificationsSwitch"></td>
<td><span class="help-mark" title="enable this if you want a URL to be requested whenever a motion event is detected">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Web Hook URL</span></td>
- <td class="settings-item-value"><input type="text" class="styled notifications" id="webHookUrlEntry" placeholder="e.g. http://example.com/notify/"></td>
+ <td class="settings-item-value"><input type="text" class="styled notifications camera-config" id="webHookUrlEntry" placeholder="e.g. http://example.com/notify/"></td>
<td><span class="help-mark" title="a URL to be requested when motion is detected; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %q = frame number">?</span></td>
</tr>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">HTTP Method</span></td>
<td class="settings-item-value">
- <select class="styled notifications" id="webHookHttpMethodSelect">
+ <select class="styled notifications camera-config" id="webHookHttpMethodSelect">
<option value="GET">GET</option>
<option value="POST">POST</option>
</select>
</tr>
<tr class="settings-item advanced-setting">
<td class="settings-item-label"><span class="settings-item-label">Run A Command</span></td>
- <td class="settings-item-value"><input type="checkbox" class="styled notifications" id="commandNotificationsSwitch"></td>
+ <td class="settings-item-value"><input type="checkbox" class="styled notifications camera-config" id="commandNotificationsSwitch"></td>
<td><span class="help-mark" title="enable this if you want to execute a command whenever a motion event is detected">?</span></td>
</tr>
- <tr class="settings-item advanced-setting">
+ <tr class="settings-item advanced-setting" required="true" strip="true">
<td class="settings-item-label"><span class="settings-item-label">Command</span></td>
- <td class="settings-item-value"><input type="text" class="styled notifications" id="commandNotificationsEntry" placeholder="command..."></td>
+ <td class="settings-item-value"><input type="text" class="styled notifications camera-config" id="commandNotificationsEntry" placeholder="command..."></td>
<td><span class="help-mark" title="a command to be executed when motion is detected; multiple commands can be separated by a semicolon; the following special tokens are accepted: %Y = year, %m = month, %d = date, %H = hour, %M = minute, %S = second, %q = frame number">?</span></td>
</tr>
+ {% for config in camera_sections.get('notifications', {}).get('configs', []) %}
+ {{config_item(config)}}
+ {% endfor %}
</table>
- <div class="settings-section-title"><input type="checkbox" class="styled section working-schedule" id="workingScheduleSwitch">Working Schedule</div>
+ <div class="settings-section-title"><input type="checkbox" class="styled section working-schedule camera-config" id="workingScheduleSwitch">Working Schedule</div>
<table class="settings">
<tr class="settings-item">
<td class="settings-item-label"><span class="settings-item-label">Monday</span></td>
<td class="settings-item-value">
- <input type="checkbox" class="styled working-schedule" id="mondayEnabledSwitch">
- <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="mondayFromEntry">
- <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="mondayToEntry">
+ <input type="checkbox" class="styled working-schedule camera-config" id="mondayEnabledSwitch">
+ <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="mondayFromEntry">
+ <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="mondayToEntry">
</td>
<td><span class="help-mark" title="sets the working schedule time interval for Mondays">?</span></td>
</tr>
<tr class="settings-item">
<td class="settings-item-label"><span class="settings-item-label">Tuesday</span></td>
<td class="settings-item-value">
- <input type="checkbox" class="styled working-schedule" id="tuesdayEnabledSwitch">
- <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="tuesdayFromEntry">
- <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="tuesdayToEntry">
+ <input type="checkbox" class="styled working-schedule camera-config" id="tuesdayEnabledSwitch">
+ <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="tuesdayFromEntry">
+ <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="tuesdayToEntry">
</td>
<td><span class="help-mark" title="sets the working schedule time interval for Tuesdays">?</span></td>
</tr>
<tr class="settings-item">
<td class="settings-item-label"><span class="settings-item-label">Wednesday</span></td>
<td class="settings-item-value">
- <input type="checkbox" class="styled working-schedule" id="wednesdayEnabledSwitch">
- <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="wednesdayFromEntry">
- <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="wednesdayToEntry">
+ <input type="checkbox" class="styled working-schedule camera-config" id="wednesdayEnabledSwitch">
+ <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="wednesdayFromEntry">
+ <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="wednesdayToEntry">
</td>
<td><span class="help-mark" title="sets the working schedule time interval for Wednesdays">?</span></td>
</tr>
<tr class="settings-item">
<td class="settings-item-label"><span class="settings-item-label">Thursday</span></td>
<td class="settings-item-value">
- <input type="checkbox" class="styled working-schedule" id="thursdayEnabledSwitch">
- <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="thursdayFromEntry">
- <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="thursdayToEntry">
+ <input type="checkbox" class="styled working-schedule camera-config" id="thursdayEnabledSwitch">
+ <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="thursdayFromEntry">
+ <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="thursdayToEntry">
</td>
<td><span class="help-mark" title="sets the working schedule time interval for Thursdays">?</span></td>
</tr>
<tr class="settings-item">
<td class="settings-item-label"><span class="settings-item-label">Friday</span></td>
<td class="settings-item-value">
- <input type="checkbox" class="styled working-schedule" id="fridayEnabledSwitch">
- <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="fridayFromEntry">
- <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="fridayToEntry">
+ <input type="checkbox" class="styled working-schedule camera-config" id="fridayEnabledSwitch">
+ <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="fridayFromEntry">
+ <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="fridayToEntry">
</td>
<td><span class="help-mark" title="sets the working schedule time interval for Friday">?</span></td>
</tr>
<tr class="settings-item">
<td class="settings-item-label"><span class="settings-item-label">Saturday</span></td>
<td class="settings-item-value">
- <input type="checkbox" class="styled working-schedule" id="saturdayEnabledSwitch">
- <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="saturdayFromEntry">
- <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="saturdayToEntry">
+ <input type="checkbox" class="styled working-schedule camera-config" id="saturdayEnabledSwitch">
+ <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="saturdayFromEntry">
+ <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="saturdayToEntry">
</td>
<td><span class="help-mark" title="sets the working schedule time interval for Saturday">?</span></td>
</tr>
<tr class="settings-item">
<td class="settings-item-label"><span class="settings-item-label">Sunday</span></td>
<td class="settings-item-value">
- <input type="checkbox" class="styled working-schedule" id="sundayEnabledSwitch">
- <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time" id="sundayFromEntry">
- <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time" id="sundayToEntry">
+ <input type="checkbox" class="styled working-schedule camera-config" id="sundayEnabledSwitch">
+ <span class="settings-item-unit time">from</span><input type="text" class="styled working-schedule time camera-config" id="sundayFromEntry">
+ <span class="settings-item-unit time">to</span><input type="text" class="styled working-schedule time camera-config" id="sundayToEntry">
</td>
<td><span class="help-mark" title="sets the working schedule time interval for Sunday">?</span></td>
</tr>
<tr class="settings-item">
<td class="settings-item-label"><span class="settings-item-label">Detect Motion</span></td>
<td class="settings-item-value">
- <select class="styled working-schedule" id="workingScheduleTypeSelect">
+ <select class="styled working-schedule camera-config" id="workingScheduleTypeSelect">
<option value="during">During Working Schedule</option>
<option value="outside">Outside Working Schedule</option>
</select>
</td>
<td><span class="help-mark" title="sets whether motion detection should be active during or outside the working schedule">?</span></td>
</tr>
+ {% for config in camera_sections.get('working-schedule', {}).get('configs', []) %}
+ {{config_item(config)}}
+ {% endfor %}
+ </table>
+
+ {% for section in camera_sections.values() %}
+ {% if section.get('label') and section.get('configs') %}
+ <div class="settings-section-title {% if section['advanced'] %}advanced-setting{% endif %}" title="{{section.get('description') or ''}}">{% if section.get('onoff') %}
+ <input type="checkbox" class="styled section additional-section {{section['name']}} camera-config" id="{{section['name']}}Switch">{% endif %}{{section['label']}}</div>
+ <table class="settings">
+ {% for config in section['configs'] %}
+ {{config_item(config)}}
+ {% endfor %}
</table>
+ {% endif %}
+ {% endfor %}
<div class="settings-progress"></div>
</div>