Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ your production-ready server of choice.

## Screenshots

The index lists available UPS devices, along with their description,
The index lists available UPS devices on each server, along with their description,
status, and battery charge:

![Index](screenshots/ups_index.png "Index")
Expand All @@ -27,3 +27,5 @@ status indicator, as well as the values of all variables set on the
device:

![UPS View](screenshots/ups_view.png "UPS View")

Clicking on the power icon will return to the index page.
Binary file modified screenshots/ups_index.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/ups_view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions webnut/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ def main(global_config, **settings):
config = Configurator(settings=settings)
config.include('pyramid_chameleon')
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_route("favicon", "/favicon.ico")
config.add_route('home', '/')
config.add_route('ups_view', '/{ups}')
config.add_view('webnut.views.notfound',
renderer='webnut:templates/404.pt',
context='pyramid.exceptions.NotFound')
renderer='webnut:templates/404.pt',
context='pyramid.exceptions.NotFound')
config.scan()
return config.make_wsgi_app()
12 changes: 12 additions & 0 deletions webnut/config.example.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# User-defined configuration.
# Rename this to config.py.

# Single server configuration, backwards compatible:

server = '127.0.0.1'
port = 3493
username = None
password = None

# Multi-server configuration:

from .webnut import NUTServer

servers = [
NUTServer('192.168.1.3', 3493, None, None),
NUTServer('192.168.1.6', 3493, None, None)
]
Binary file added webnut/static/favicon.ico
Binary file not shown.
1 change: 0 additions & 1 deletion webnut/templates/404.pt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<div metal:use-macro="view.layout">
<div metal:fill-slot="content">
<br>
<h3>404 Not Found</h3>
Sorry, no UPS with that name exists.
</div>
Expand Down
17 changes: 12 additions & 5 deletions webnut/templates/index.pt
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
<div metal:use-macro="view.layout">
<div metal:fill-slot="content">
<br>
<table class="table table-striped table-hover" id="ups_table">
<thead>
<tr>
<th>Name</th>
<th>Server</th>
<th>Description</th>
<th>Status</th>
<th>Battery</th>
<th>Runtime</th>
</tr>
</thead>
<tbody>
<tr tal:repeat="ups ups_list">
<td>
<a href="${request.route_url('ups_view', ups=ups)}">${ups}</a>
<a title="${ups_list[ups]['name']} detail" href="${request.route_url('ups_view', ups=ups)}">${ups_list[ups]['name']}</a>
</td>
<td>${ups_list[ups]['server']}</td>
<td>${ups_list[ups]['description']}</td>
<td>${ups_list[ups]['status']}</td>
<td>${ups_list[ups]['battery']}%</td>
<td>${ups_list[ups]['battery']}</td>
<td>${ups_list[ups]['runtime']}</td>
</tr>
</tbody>
</table>

<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/1.10-dev/js/jquery.dataTables.min.js"></script>
<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/plug-ins/28e7751dbec/integration/bootstrap/3/dataTables.bootstrap.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.1/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.24/css/dataTables.bootstrap4.min.css"/>
<script type="text/javascript" charset="utf8" src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.24/js/jquery.dataTables.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.24/js/dataTables.bootstrap4.min.js"></script>
<script type="text/javascript">
$(function(){
$("#ups_table").dataTable();
Expand Down
22 changes: 12 additions & 10 deletions webnut/templates/layout.pt
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,25 @@
metal:define-macro="layout">
<head>
<title>${title} - webNUT</title>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css"/>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css"/>
<link rel="stylesheet" href="//cdn.datatables.net/plug-ins/28e7751dbec/integration/bootstrap/3/dataTables.bootstrap.css"/>
<script type="text/javascript" src="//code.jquery.com/jquery.min.js"></script>
<script type="text/javascript" src="//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.2/css/all.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.2/css/v4-shims.css">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.1/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.24/css/dataTables.bootstrap4.min.css"/>
<script type="text/javascript" charset="utf8" src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.24/js/jquery.dataTables.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.24/js/dataTables.bootstrap4.min.js"></script>
</head>
<body>
<div id="main">
<div class="col-sm-8 col-sm-offset-2 col-xs-12 col-xs-offset-0">
<div class="col-sm-8 offset-sm-2 col-xs-12 offset-xs-0 mt-3 mb-5">
<div class="row">
<h2><i class="fa fa-power-off"></i> ${title}</h2>
<h2><a href="/" title="Device list"><i class="fa fa-power-off"></i></a>&nbsp;${title}</h2>
</div>
<div class="row mt-3">
<div metal:define-slot="content">
</div>
</div>
<div class="row">
<br><br><br>
</div>
</div>
</div>
</body>
Expand Down
24 changes: 18 additions & 6 deletions webnut/templates/ups_view.pt
Original file line number Diff line number Diff line change
@@ -1,29 +1,41 @@
<div metal:use-macro="view.layout">
<div metal:fill-slot="content">
<strong>Status</strong>: ${ups_status}
<br>
<strong>Battery charge</strong>: ${ups_vars['battery.charge'][0]}%
<br>
<div class="col-sm-8 col-xs-8 pl-0 mb-2">
<div class="row">
<div class="col-4"><strong>Status</strong>:</div>
<div class="col-2">${ups_status}</div>
</div>
<div class="row">
<div class="col-4"><strong>Battery charge</strong>:</div>
<div class="col-2">${ups_battery}</div>
</div>
</div>
<h3>UPS Variables</h3>
<table class="table table-striped table-hover" id="var_table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Value</th>
<th>Writable</th>
</tr>
</thead>
<tbody>
<tr tal:repeat="var ups_vars">
<td>${var}</td>
<td>${ups_vars[var][1]}</td>
<td>${ups_vars[var][0]}</td>
<td class="text-center">${ups_vars[var][2]}</td>
</tr>
</tbody>
</table>

<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/1.10-dev/js/jquery.dataTables.min.js"></script>
<script type="text/javascript" charset="utf8" src="//cdn.datatables.net/plug-ins/28e7751dbec/integration/bootstrap/3/dataTables.bootstrap.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.1/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.24/css/dataTables.bootstrap4.min.css"/>
<script type="text/javascript" charset="utf8" src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.24/js/jquery.dataTables.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.24/js/dataTables.bootstrap4.min.js"></script>
<script type="text/javascript">
$(function(){
$("#var_table").dataTable();
Expand Down
108 changes: 102 additions & 6 deletions webnut/views.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,130 @@
import os

from pyramid.exceptions import NotFound
from pyramid.renderers import get_renderer
from pyramid.view import view_config
from pyramid.response import FileResponse

from .webnut import WebNUT
from .webnut import NUTServer

from . import config


class NUTViews(object):
def __init__(self, request):
self.request = request
renderer = get_renderer("templates/layout.pt")
self.layout = renderer.implementation().macros['layout']
self.webnut = WebNUT(config.server, config.port,
config.username, config.password)
# Backward compatible support for single server configuration
servers = []
if hasattr(config, 'servers'):
servers = config.servers
if (hasattr(config, 'server') and hasattr(config, 'port') and
hasattr(config, 'username') and hasattr(config, 'password')):
servers.append(NUTServer(config.server, config.port,
config.username, config.password))
self.webnut = WebNUT(servers)

@view_config(route_name="favicon")
def favicon_view(self):
here = os.path.dirname(__file__)
icon = os.path.join(here, "static", "favicon.ico")
return FileResponse(icon, request=self.request)

@view_config(route_name='home', renderer='templates/index.pt')
def home(self):
return dict(title='UPS Devices',
ups_list=self.webnut.get_ups_list())
ups_list = self.webnut.get_ups_list()
# Update some list values with custom HTML rendering.
for (_, ups_vars) in ups_list.items():
ups_vars['status'] = self._ups_status(ups_vars['status'])
ups_vars['battery'] = self._ups_battery(ups_vars['battery'])
return dict(title='UPS Devices', ups_list=ups_list)

@view_config(route_name='ups_view', renderer='templates/ups_view.pt')
def ups_view(self):
ups = self.request.matchdict['ups']
try:
ups_name = self.webnut.get_ups_name(ups)
ups_vars = self.webnut.get_ups_vars(ups)
return dict(title=ups_name, ups_vars=ups_vars[0],
ups_status=ups_vars[1])
ups_status = ups_vars.get('ups.status', ('Unknown', ''))[0]
ups_battery = int(ups_vars.get('battery.charge', (0, ''))[0])
# Update vars with custom HTML rendering.
for (k, v) in ups_vars.items():
ups_vars[k] = (v[0], v[1], self._ups_var_writable(v[2]))
return dict(title=ups_name,
ups_vars=ups_vars,
ups_status=self._ups_status(ups_status),
ups_battery=self._ups_battery(ups_battery))
except KeyError:
raise NotFound

def _ups_var_writable(self, writable: bool):

class ReadWrite(object):
# Allows Chameleon to print unescaped HTML.
def __init__(self, writable: bool):
self.writable = writable

def __html__(self):
if not writable:
return ''
return '''<i class="fas fa-pen-square fa-lg text-success" title="Writable"></i>'''

return ReadWrite(writable)

def _ups_status(self, status: str):

class Status(object):
# Allows Chameleon to print unescaped HTML.
def __init__(self, icon, color, title):
self.icon = icon
self.color = color
self.title = title

def __html__(self):
return '''
<i class="fa fa-{0}" style="color:{1}" title="{2}"></i>
'''.format(self.icon, self.color, self.title)

if status.startswith('OL'):
return Status('check', 'green', 'Online')
elif status.startswith('OB'):
return Status('warning', 'orange', 'On Battery')
elif status.startswith('LB'):
return Status('warning', 'red', 'Low Battery')
return Status('unknown', 'black', 'Unknown')

def _ups_battery(self, charge: int):

class Status(object):
# Allows Chameleon to print unescaped HTML.
def __init__(self, charge, color):
self.charge = charge
self.color = color

def __html__(self):
height = 20
return '''
<div class="progress position-relative" style="height:{0}px;background-color:silver;line-height:{0}px;">
<div class="progress-bar {1}"
style="width:{2}%;color:black"
role="progressbar";
aria-valuenow="{2}"
aria-valuemin="0"
aria-valuemax="100"></div>
<span title="{2}%" class="justify-content-center align-middle d-flex position-absolute w-100">{2}%</span>
</div>
'''.format(height, self.color, self.charge)

if charge > 90:
return Status(charge, 'bg-success')
elif charge > 60:
return Status(charge, 'bg-warning')
else:
return Status(charge, 'bg-danger')


def notfound(request):
request.response.status = 404
return dict(title='No Such UPS')
Loading