mirror of
https://github.com/ShaYmez/HBmonitor.git
synced 2024-11-21 23:45:17 -05:00
Initial Commit
This commit is contained in:
commit
00f03510ce
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
*.pyc
|
||||||
|
config.py
|
||||||
|
*.csv
|
40
README.md
Normal file
40
README.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
### FOR SUPPORT, DISCUSSION, GETTING INVOLVED ###
|
||||||
|
|
||||||
|
Please join the DVSwitch group at groups.io for online forum support, discussion, and to become part of the development team.
|
||||||
|
|
||||||
|
DVSwitch@groups.io
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Socket-Based Reporting for HBlink**
|
||||||
|
|
||||||
|
Over the years, the biggest request recevied for HBlink (other than call-routing/bridging tools) has been web-based diagnostics and/or statistics for the program.
|
||||||
|
|
||||||
|
I strongly disagree with including the amount of overhead this would require inside HBlink -- which still runs nicely on very modest resources. That it does this, and is in Python is a point of pride for me... Just let me have this one, ok? What I have done is added some hooks to HBlink, which will be expanded over time, whereby it listens on a TCP socket and provides the raw data necessary for a "web dashboard", or really any external logging or statistics gathering program.
|
||||||
|
|
||||||
|
HBmonitor is my take on a "web dashboard" for HBlink.
|
||||||
|
|
||||||
|
***THIS SOFTWARE IS VERY, VERY NEW***
|
||||||
|
|
||||||
|
Right now, I'm just getting into how this should work, what does work well, what does not... and I am NOT a web applications programmer, so yeah, that javascript stuff is gonna look bad. Know what you're doing? Help me!
|
||||||
|
|
||||||
|
It has now reached a point where folks who know what they're doing can probably make it work reasonably well, so I'm opening up the project to the public.
|
||||||
|
|
||||||
|
***GOALS OF THE PROJECT***
|
||||||
|
|
||||||
|
Some things I'm going to stick to pretty closely. Here they are:
|
||||||
|
|
||||||
|
+ HBmonitor be one process that includes a webserver
|
||||||
|
+ Websockets are used for pushing data to the browser - no long-polling, etc.
|
||||||
|
+ Does not provide data that's easily misunderstood
|
||||||
|
|
||||||
|
***0x49 DE N0MJS***
|
||||||
|
|
||||||
|
Copyright (C) 2013-2018 Cortney T. Buffington, N0MJS <n0mjs@me.com>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
18
config_SAMPLE.py
Normal file
18
config_SAMPLE.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
REPORT_NAME = 'system.domain.name' # Name of the monitored HBlink system
|
||||||
|
CONFIG_INC = True # Include HBlink stats
|
||||||
|
BRIDGES_INC = True # Include Bridge stats (confbrige.py)
|
||||||
|
DMRLINK_IP = '127.0.0.1' # HBlink's IP Address
|
||||||
|
DMRLINK_PORT = 4321 # HBlink's TCP reporting socket
|
||||||
|
FREQUENCY = 10 # Frequency to push updates to web clients
|
||||||
|
WEB_SERVER_PORT = 8080 # Has to be above 1024 if you're not running as root
|
||||||
|
|
||||||
|
# Files and stuff for loading alias files for mapping numbers to names
|
||||||
|
PATH = './' # MUST END IN '/'
|
||||||
|
PEER_FILE = 'peer_ids.csv' # Will auto-download from DMR-MARC
|
||||||
|
SUBSCRIBER_FILE = 'subscriber_ids.csv' # Will auto-download from DMR-MARC
|
||||||
|
TGID_FILE = 'talkgroup_ids.csv' # User provided, should be in "integer TGID, TGID name" format
|
||||||
|
LOCAL_SUB_FILE = 'local_subscriber_ids.csv' # User provided (optional, leave '' if you don't use it), follow the format of DMR-MARC
|
||||||
|
LOCAL_PEER_FILE = 'local_peer_ids.csv' # User provided (optional, leave '' if you don't use it), follow the format of DMR-MARC
|
||||||
|
FILE_RELOAD = 7 # Number of days before we reload DMR-MARC database files
|
||||||
|
PEER_URL = 'http://radioid.net/static/rptrs.csv'
|
||||||
|
SUBSCRIBER_URL = 'http://radioid.net/static/users.csv'
|
83
index_template.html
Normal file
83
index_template.html
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var sock = null;
|
||||||
|
var ellog = null;
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
var wsuri;
|
||||||
|
|
||||||
|
ellog = document.getElementById('log');
|
||||||
|
hblink_table = document.getElementById('hblink');
|
||||||
|
confbridge_table = document.getElementById('bridge');
|
||||||
|
|
||||||
|
wsuri = "ws://" + window.location.hostname + ":9000";
|
||||||
|
|
||||||
|
|
||||||
|
if ("WebSocket" in window) {
|
||||||
|
sock = new WebSocket(wsuri);
|
||||||
|
} else if ("MozWebSocket" in window) {
|
||||||
|
sock = new MozWebSocket(wsuri);
|
||||||
|
} else {
|
||||||
|
log("Browser does not support WebSocket!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sock) {
|
||||||
|
sock.onopen = function() {
|
||||||
|
log("Connected to " + wsuri);
|
||||||
|
}
|
||||||
|
sock.onclose = function(e) {
|
||||||
|
log("Connection closed (wasClean = " + e.wasClean + ", code = " + e.code + ", reason = '" + e.reason + "')");
|
||||||
|
hblink_table.innerHTML = "";
|
||||||
|
confbridge_table.innerHTML = "";
|
||||||
|
sock = null;
|
||||||
|
}
|
||||||
|
sock.onmessage = function(e) {
|
||||||
|
var opcode = e.data.slice(0,1);
|
||||||
|
var message = e.data.slice(1);
|
||||||
|
if (opcode == "d") {
|
||||||
|
hblink(message);
|
||||||
|
} else if (opcode == "b") {
|
||||||
|
confbridge(message);
|
||||||
|
} else if (opcode == "l") {
|
||||||
|
log(message);
|
||||||
|
} else if (opcode == "q") {
|
||||||
|
log(message);
|
||||||
|
hblink_table.innerHTML = "";
|
||||||
|
confbridge_table.innerHTML = "";
|
||||||
|
} else {
|
||||||
|
log("Unknown Message Received: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function hblink(_msg) {
|
||||||
|
hblink_table.innerHTML = _msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
function confbridge(_msg) {
|
||||||
|
confbridge_table.innerHTML = _msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(_msg) {
|
||||||
|
ellog.innerHTML += _msg + '\n';
|
||||||
|
ellog.scrollTop = ellog.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body style="font: 10pt arial, sans-serif">
|
||||||
|
<h1><center>HBlink Monitoring Server</center></h1>
|
||||||
|
<h3><center><<<system_name>>></center></h3>
|
||||||
|
<hr>
|
||||||
|
<noscript>You must enable JavaScript</noscript>
|
||||||
|
<style>table, td, th {border: .5px solid black; padding: 2px; border-collapse: collapse; text-align:center;}</style>
|
||||||
|
<p id="hblink"></p>
|
||||||
|
<p id="bridge"></p>
|
||||||
|
<hr>
|
||||||
|
<h3>Event Log Window:</h3>
|
||||||
|
<pre id="log" style="height: 10em; overflow-y: scroll; background-color: #ccc;"></pre>
|
||||||
|
</body>
|
||||||
|
</html>
|
0
logfile.log
Normal file
0
logfile.log
Normal file
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Twisted>=12.0.0
|
||||||
|
dmr_utils>=0.1.7
|
||||||
|
bitstring>=3.1.3
|
||||||
|
autobahn>=0.17.2
|
||||||
|
jinja2>=2.9.6
|
39
templates/bridge_table.html
Normal file
39
templates/bridge_table.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<hr>
|
||||||
|
<h3>Bridge Group Status Tables:</h3>
|
||||||
|
{% for _bridge, _bridge_data in _table.iteritems() %}
|
||||||
|
<table style="width:100%; font: 10pt arial, sans-serif">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 10%" />
|
||||||
|
<col style="width: 5%" />
|
||||||
|
<col style="width: 5%" />
|
||||||
|
<col style="width: 10%" />
|
||||||
|
<col style="width: 10%" />
|
||||||
|
<col style="width: 10%" />
|
||||||
|
<col style="width: 25%" />
|
||||||
|
<col style="width: 25%" />
|
||||||
|
</colgroup>
|
||||||
|
<h4>Conference Bridge: {{ _bridge }}</h4>
|
||||||
|
<tr style="width:100%; font: 10pt arial, sans-serif; background-color:#666666; color:white">
|
||||||
|
<th>System</th>
|
||||||
|
<th>Slot</th>
|
||||||
|
<th>TGID</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Timeout</th>
|
||||||
|
<th>Timeout Action</th>
|
||||||
|
<th>Connect TGIDs</th>
|
||||||
|
<th>Disconnect TGIDs</th>
|
||||||
|
</tr>
|
||||||
|
{% for system, _system_data in _table[_bridge].iteritems() %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ system }}</td>
|
||||||
|
<td>{{ _table[_bridge][system]['TS'] }}</td>
|
||||||
|
<td>{{ _table[_bridge][system]['TGID'] }}</td>
|
||||||
|
<td style="background-color:{{ _table[_bridge][system]['COLOR'] }}">{{ _table[_bridge][system]['ACTIVE'] }}</td>
|
||||||
|
<td>{{ _table[_bridge][system]['EXP_TIME'] }}</td>
|
||||||
|
<td>{{ _table[_bridge][system]['TO_ACTION'] }}</td>
|
||||||
|
<td>{{ _table[_bridge][system]['TRIG_ON'] }}</td>
|
||||||
|
<td>{{ _table[_bridge][system]['TRIG_OFF'] }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endfor %}
|
72
templates/hblink_table.html
Normal file
72
templates/hblink_table.html
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<h3>HBlink Status Tables:</h3>
|
||||||
|
<h4>Master Systems</h4>
|
||||||
|
<table style="width:100%; font: 10pt arial, sans-serif">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 20%" />
|
||||||
|
<col style="width: 15%" />
|
||||||
|
<col style="width: 15%" />
|
||||||
|
<col style="width: 10%" />
|
||||||
|
<col style="width: 15%" />
|
||||||
|
<col style="width: 15%" />
|
||||||
|
<col style="width: 10%" />
|
||||||
|
</colgroup>
|
||||||
|
<tr style="width:100%; font: 10pt arial, sans-serif; background-color:#666666; color:white">
|
||||||
|
<th>HBP System</th>
|
||||||
|
<th>Client Radio ID</th>
|
||||||
|
<th>Callsign</th>
|
||||||
|
<th>Connection</th>
|
||||||
|
<th>Pings</br>Received</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>Port</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% for _master in _table['MASTERS'] %}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight:bold" rowspan="{{ _table['MASTERS'][_master]|length * 2 }}"> {{ _master}} </td>
|
||||||
|
{% for _client, _cdata in _table['MASTERS'][_master]['CLIENTS'].iteritems() %}
|
||||||
|
<td>{{ _client }}</td>
|
||||||
|
<td>{{ _cdata['CALLSIGN'] }}</td>
|
||||||
|
<td style={{ 'background-color:#00ff00' if _cdata['CONNECTION'] == 'YES' else ';background-color:#ff0000' }}>{{ _cdata['CONNECTION'] }}</td>
|
||||||
|
<td>{{ _cdata['PINGS_RECEIVED'] }}</td>
|
||||||
|
<td>{{ _cdata['IP'] }}</td>
|
||||||
|
<td>{{ _cdata['PORT'] }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>Client Systems</h4>
|
||||||
|
<table style="width:100%; font: 10pt arial, sans-serif">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 20%" />
|
||||||
|
<col style="width: 15%" />
|
||||||
|
<col style="width: 15%" />
|
||||||
|
<col style="width: 10%" />
|
||||||
|
<col style="width: 15%" />
|
||||||
|
<col style="width: 15%" />
|
||||||
|
<col style="width: 10%" />
|
||||||
|
</colgroup>
|
||||||
|
<tr style="width:100%; font: 10pt arial, sans-serif; background-color:#666666; color:white">
|
||||||
|
<th>HBP System</th>
|
||||||
|
<th>Client Radio ID</th>
|
||||||
|
<th>Callsign</th>
|
||||||
|
<th>Connection</th>
|
||||||
|
<th>Ping Sent</th>
|
||||||
|
<th>Ping Ack</th>
|
||||||
|
<th>Master</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% for _client in _table['CLIENTS'] %}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td style="font-weight:bold"> {{ _client}} </td>
|
||||||
|
<td>{{ _table['CLIENTS'][_client]['RADIO_ID'] }}</td>
|
||||||
|
<td>{{ _table['CLIENTS'][_client]['CALLSIGN'] }}</td>
|
||||||
|
<td style={{ 'background-color:#00ff00' if _table['CLIENTS'][_client]['STATS']['CONNECTION'] == 'YES' else ';background-color:#ff0000' }}>{{ _table['CLIENTS'][_client]['STATS']['CONNECTION'] }}</td>
|
||||||
|
<td>{{ _table['CLIENTS'][_client]['STATS']['PINGS_SENT'] }}</td>
|
||||||
|
<td>{{ _table['CLIENTS'][_client]['STATS']['PINGS_ACKD'] }}</td>
|
||||||
|
<td>{{ _table['CLIENTS'][_client]['MASTER_IP'] }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
415
web_tables.py
Executable file
415
web_tables.py
Executable file
@ -0,0 +1,415 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
###############################################################################
|
||||||
|
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software Foundation,
|
||||||
|
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
# Standard modules
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Twisted modules
|
||||||
|
from twisted.internet.protocol import ReconnectingClientFactory, Protocol
|
||||||
|
from twisted.protocols.basic import NetstringReceiver
|
||||||
|
from twisted.internet import reactor, task
|
||||||
|
from twisted.web.server import Site
|
||||||
|
from twisted.web.static import File
|
||||||
|
from twisted.web.resource import Resource
|
||||||
|
|
||||||
|
# Autobahn provides websocket service under Twisted
|
||||||
|
from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory
|
||||||
|
|
||||||
|
# Specific functions to import from standard modules
|
||||||
|
from pprint import pprint
|
||||||
|
from time import time, strftime, localtime
|
||||||
|
from cPickle import loads
|
||||||
|
from binascii import b2a_hex as h
|
||||||
|
from os.path import getmtime
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
# Web templating environment
|
||||||
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||||
|
|
||||||
|
# Utilities from K0USY Group sister project
|
||||||
|
from dmr_utils.utils import int_id, get_alias, try_download, mk_full_id_dict
|
||||||
|
|
||||||
|
# Configuration variables and IPSC constants
|
||||||
|
from config import *
|
||||||
|
#from ipsc_const import *
|
||||||
|
|
||||||
|
# Opcodes for reporting protocol to HBlink
|
||||||
|
OPCODE = {
|
||||||
|
'CONFIG_REQ': '\x00',
|
||||||
|
'CONFIG_SND': '\x01',
|
||||||
|
'BRIDGE_REQ': '\x02',
|
||||||
|
'BRIDGE_SND': '\x03',
|
||||||
|
'CONFIG_UPD': '\x04',
|
||||||
|
'BRIDGE_UPD': '\x05',
|
||||||
|
'LINK_EVENT': '\x06',
|
||||||
|
'BRDG_EVENT': '\x07',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global Variables:
|
||||||
|
CONFIG = {}
|
||||||
|
CTABLE = {'MASTERS': {}, 'CLIENTS': {}}
|
||||||
|
BRIDGES = {}
|
||||||
|
BTABLE = {}
|
||||||
|
BTABLE['BRIDGES'] = {}
|
||||||
|
BRIDGES_RX = ''
|
||||||
|
CONFIG_RX = ''
|
||||||
|
LOGBUF = deque(100*[''], 100)
|
||||||
|
RED = '#ff0000'
|
||||||
|
GREEN = '#00ff00'
|
||||||
|
BLUE = '#0000ff'
|
||||||
|
ORANGE = '#ff8000'
|
||||||
|
WHITE = '#ffffff'
|
||||||
|
|
||||||
|
|
||||||
|
# For importing HTML templates
|
||||||
|
def get_template(_file):
|
||||||
|
with open(_file, 'r') as html:
|
||||||
|
return html.read()
|
||||||
|
|
||||||
|
# Alias string processor
|
||||||
|
def alias_string(_id, _dict):
|
||||||
|
alias = get_alias(_id, _dict, 'CALLSIGN', 'CITY', 'STATE')
|
||||||
|
if type(alias) == list:
|
||||||
|
for x,item in enumerate(alias):
|
||||||
|
if item == None:
|
||||||
|
alias.pop(x)
|
||||||
|
return ', '.join(alias)
|
||||||
|
else:
|
||||||
|
return alias
|
||||||
|
|
||||||
|
# Build the HBlink connections table
|
||||||
|
def build_hblink_table(_config):
|
||||||
|
_stats_table = {'MASTERS': {}, 'CLIENTS': {}}
|
||||||
|
for _hbp, _hbp_data in _config.iteritems():
|
||||||
|
if _hbp_data['ENABLED'] == True:
|
||||||
|
if _hbp_data['MODE'] == 'MASTER':
|
||||||
|
_stats_table['MASTERS'][_hbp] = {}
|
||||||
|
_stats_table['MASTERS'][_hbp]['REPEAT'] = _hbp_data['REPEAT']
|
||||||
|
_stats_table['MASTERS'][_hbp]['CLIENTS'] = {}
|
||||||
|
for _client in _hbp_data['CLIENTS']:
|
||||||
|
_stats_table['MASTERS'][_hbp]['CLIENTS'][int_id(_client)] = {}
|
||||||
|
_stats_table['MASTERS'][_hbp]['CLIENTS'][int_id(_client)]['CALLSIGN'] = _hbp_data['CLIENTS'][_client]['CALLSIGN']
|
||||||
|
_stats_table['MASTERS'][_hbp]['CLIENTS'][int_id(_client)]['CONNECTION'] = _hbp_data['CLIENTS'][_client]['CONNECTION']
|
||||||
|
_stats_table['MASTERS'][_hbp]['CLIENTS'][int_id(_client)]['IP'] = _hbp_data['CLIENTS'][_client]['IP']
|
||||||
|
_stats_table['MASTERS'][_hbp]['CLIENTS'][int_id(_client)]['PINGS_RECEIVED'] = _hbp_data['CLIENTS'][_client]['PINGS_RECEIVED']
|
||||||
|
_stats_table['MASTERS'][_hbp]['CLIENTS'][int_id(_client)]['LAST_PING'] = _hbp_data['CLIENTS'][_client]['LAST_PING']
|
||||||
|
_stats_table['MASTERS'][_hbp]['CLIENTS'][int_id(_client)]['PORT'] = _hbp_data['CLIENTS'][_client]['PORT']
|
||||||
|
elif _hbp_data['MODE'] == 'CLIENT':
|
||||||
|
_stats_table['CLIENTS'][_hbp] = {}
|
||||||
|
_stats_table['CLIENTS'][_hbp]['CALLSIGN'] = _hbp_data['CALLSIGN']
|
||||||
|
_stats_table['CLIENTS'][_hbp]['RADIO_ID'] = int_id(_hbp_data['RADIO_ID'])
|
||||||
|
_stats_table['CLIENTS'][_hbp]['MASTER_IP'] = _hbp_data['MASTER_IP']
|
||||||
|
_stats_table['CLIENTS'][_hbp]['STATS'] = _hbp_data['STATS']
|
||||||
|
|
||||||
|
return(_stats_table)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# CONFBRIDGE TABLE FUNCTIONS
|
||||||
|
#
|
||||||
|
def build_bridge_table(_bridges):
|
||||||
|
_stats_table = {}
|
||||||
|
_now = time()
|
||||||
|
_cnow = strftime('%Y-%m-%d %H:%M:%S', localtime(_now))
|
||||||
|
|
||||||
|
for _bridge, _bridge_data in _bridges.iteritems():
|
||||||
|
_stats_table[_bridge] = {}
|
||||||
|
|
||||||
|
for system in _bridges[_bridge]:
|
||||||
|
_stats_table[_bridge][system['SYSTEM']] = {}
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['TS'] = system['TS']
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['TGID'] = int_id(system['TGID'])
|
||||||
|
|
||||||
|
if system['TO_TYPE'] == 'ON' or system['TO_TYPE'] == 'OFF':
|
||||||
|
if system['TIMER'] - _now > 0:
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['EXP_TIME'] = int(system['TIMER'] - _now)
|
||||||
|
else:
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['EXP_TIME'] = 'Expired'
|
||||||
|
if system['TO_TYPE'] == 'ON':
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['TO_ACTION'] = 'Disconnect'
|
||||||
|
else:
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['TO_ACTION'] = 'Connect'
|
||||||
|
else:
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['EXP_TIME'] = 'N/A'
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['TO_ACTION'] = 'None'
|
||||||
|
|
||||||
|
if system['ACTIVE'] == True:
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['ACTIVE'] = 'Connected'
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['COLOR'] = GREEN
|
||||||
|
elif system['ACTIVE'] == False:
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['ACTIVE'] = 'Disconnected'
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['COLOR'] = RED
|
||||||
|
|
||||||
|
for i in range(len(system['ON'])):
|
||||||
|
system['ON'][i] = str(int_id(system['ON'][i]))
|
||||||
|
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['TRIG_ON'] = ', '.join(system['ON'])
|
||||||
|
|
||||||
|
for i in range(len(system['OFF'])):
|
||||||
|
system['OFF'][i] = str(int_id(system['OFF'][i]))
|
||||||
|
|
||||||
|
_stats_table[_bridge][system['SYSTEM']]['TRIG_OFF'] = ', '.join(system['OFF'])
|
||||||
|
|
||||||
|
return _stats_table
|
||||||
|
|
||||||
|
#
|
||||||
|
# BUILD HBlink AND CONFBRIDGE TABLES FROM CONFIG/BRIDGES DICTS
|
||||||
|
# THIS CURRENTLY IS A TIMED CALL
|
||||||
|
#
|
||||||
|
build_time = time()
|
||||||
|
def build_stats():
|
||||||
|
global build_time
|
||||||
|
now = time()
|
||||||
|
if True: #now > build_time + 1:
|
||||||
|
if CONFIG:
|
||||||
|
table = 'd' + dtemplate.render(_table=CTABLE)
|
||||||
|
dashboard_server.broadcast(table)
|
||||||
|
if BRIDGES:
|
||||||
|
table = 'b' + btemplate.render(_table=BTABLE['BRIDGES'])
|
||||||
|
dashboard_server.broadcast(table)
|
||||||
|
build_time = now
|
||||||
|
|
||||||
|
#
|
||||||
|
# PROCESS IN COMING MESSAGES AND TAKE THE CORRECT ACTION DEPENING ON THE OPCODE
|
||||||
|
#
|
||||||
|
def process_message(_message):
|
||||||
|
global CTABLE, CONFIG, BRIDGES, CONFIG_RX, BRIDGES_RX
|
||||||
|
opcode = _message[:1]
|
||||||
|
_now = strftime('%Y-%m-%d %H:%M:%S %Z', localtime(time()))
|
||||||
|
|
||||||
|
if opcode == OPCODE['CONFIG_SND']:
|
||||||
|
logging.debug('got CONFIG_SND opcode')
|
||||||
|
CONFIG = load_dictionary(_message)
|
||||||
|
CONFIG_RX = strftime('%Y-%m-%d %H:%M:%S', localtime(time()))
|
||||||
|
CTABLE = build_hblink_table(CONFIG)
|
||||||
|
|
||||||
|
elif opcode == OPCODE['BRIDGE_SND']:
|
||||||
|
logging.debug('got BRIDGE_SND opcode')
|
||||||
|
BRIDGES = load_dictionary(_message)
|
||||||
|
BRIDGES_RX = strftime('%Y-%m-%d %H:%M:%S', localtime(time()))
|
||||||
|
BTABLE['BRIDGES'] = build_bridge_table(BRIDGES)
|
||||||
|
|
||||||
|
elif opcode == OPCODE['LINK_EVENT']:
|
||||||
|
logging.info('LINK_EVENT Received: {}'.format(repr(_message[1:])))
|
||||||
|
|
||||||
|
elif opcode == OPCODE['BRDG_EVENT']:
|
||||||
|
logging.info('BRIDGE EVENT: {}'.format(repr(_message[1:])))
|
||||||
|
p = _message[1:].split(",")
|
||||||
|
if p[0] == 'GROUP VOICE':
|
||||||
|
if p[1] == 'END':
|
||||||
|
log_message = '{}: {} {}: System: {}; IPSC Peer: {} - {}; Subscriber: {} - {}; TS: {}; TGID: {}; Duration: {}s'.format(_now, p[0], p[1], p[2], p[4], alias_string(int(p[4]), peer_ids), p[5], alias_string(int(p[5]), subscriber_ids), p[6], p[7], p[8])
|
||||||
|
elif p[1] == 'START':
|
||||||
|
log_message = '{}: {} {}: System: {}; IPSC Peer: {} - {}; Subscriber: {} - {}; TS: {}; TGID: {}'.format(_now, p[0], p[1], p[2], p[4], alias_string(int(p[4]), peer_ids), p[5], alias_string(int(p[5]), subscriber_ids), p[6], p[7])
|
||||||
|
elif p[1] == 'END WITHOUT MATCHING START':
|
||||||
|
log_message = '{}: {} {} on IPSC System {}: IPSC Peer: {} - {}; Subscriber: {} - {}; TS: {}; TGID: {}'.format(_now, p[0], p[1], p[2], p[4], alias_string(int(p[4]), peer_ids), p[5], alias_string(int(p[5]), subscriber_ids), p[6], p[7])
|
||||||
|
else:
|
||||||
|
log_message = '{}: UNKNOWN GROUP VOICE LOG MESSAGE'.format(_now)
|
||||||
|
else:
|
||||||
|
log_message = '{}: UNKNOWN LOG MESSAGE'.format(_now)
|
||||||
|
|
||||||
|
dashboard_server.broadcast('l' + log_message)
|
||||||
|
LOGBUF.append(log_message)
|
||||||
|
else:
|
||||||
|
logging.debug('got unknown opcode: {}, message: {}'.format(repr(opcode), repr(_message[1:])))
|
||||||
|
|
||||||
|
|
||||||
|
def load_dictionary(_message):
|
||||||
|
data = _message[1:]
|
||||||
|
return loads(data)
|
||||||
|
logging.debug('Successfully decoded dictionary')
|
||||||
|
|
||||||
|
#
|
||||||
|
# COMMUNICATION WITH THE HBlink INSTANCE
|
||||||
|
#
|
||||||
|
class report(NetstringReceiver):
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def connectionMade(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def connectionLost(self, reason):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stringReceived(self, data):
|
||||||
|
process_message(data)
|
||||||
|
|
||||||
|
|
||||||
|
class reportClientFactory(ReconnectingClientFactory):
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def startedConnecting(self, connector):
|
||||||
|
logging.info('Initiating Connection to Server.')
|
||||||
|
if 'dashboard_server' in locals() or 'dashboard_server' in globals():
|
||||||
|
dashboard_server.broadcast('q' + 'Connection to HBlink Established')
|
||||||
|
|
||||||
|
def buildProtocol(self, addr):
|
||||||
|
logging.info('Connected.')
|
||||||
|
logging.info('Resetting reconnection delay')
|
||||||
|
self.resetDelay()
|
||||||
|
return report()
|
||||||
|
|
||||||
|
def clientConnectionLost(self, connector, reason):
|
||||||
|
logging.info('Lost connection. Reason: %s', reason)
|
||||||
|
ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
|
||||||
|
dashboard_server.broadcast('q' + 'Connection to HBlink Lost')
|
||||||
|
|
||||||
|
def clientConnectionFailed(self, connector, reason):
|
||||||
|
logging.info('Connection failed. Reason: %s', reason)
|
||||||
|
ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# WEBSOCKET COMMUNICATION WITH THE DASHBOARD CLIENT
|
||||||
|
#
|
||||||
|
class dashboard(WebSocketServerProtocol):
|
||||||
|
|
||||||
|
def onConnect(self, request):
|
||||||
|
logging.info('Client connecting: %s', request.peer)
|
||||||
|
|
||||||
|
def onOpen(self):
|
||||||
|
logging.info('WebSocket connection open.')
|
||||||
|
self.factory.register(self)
|
||||||
|
self.sendMessage('d' + str(dtemplate.render(_table=CTABLE)))
|
||||||
|
self.sendMessage('b' + str(btemplate.render(_table=BTABLE['BRIDGES'])))
|
||||||
|
for _message in LOGBUF:
|
||||||
|
if _message:
|
||||||
|
self.sendMessage('l' + _message)
|
||||||
|
|
||||||
|
def onMessage(self, payload, isBinary):
|
||||||
|
if isBinary:
|
||||||
|
logging.info('Binary message received: %s bytes', len(payload))
|
||||||
|
else:
|
||||||
|
logging.info('Text message received: %s', payload.decode('utf8'))
|
||||||
|
|
||||||
|
def connectionLost(self, reason):
|
||||||
|
WebSocketServerProtocol.connectionLost(self, reason)
|
||||||
|
self.factory.unregister(self)
|
||||||
|
|
||||||
|
def onClose(self, wasClean, code, reason):
|
||||||
|
logging.info('WebSocket connection closed: %s', reason)
|
||||||
|
|
||||||
|
|
||||||
|
class dashboardFactory(WebSocketServerFactory):
|
||||||
|
|
||||||
|
def __init__(self, url):
|
||||||
|
WebSocketServerFactory.__init__(self, url)
|
||||||
|
self.clients = []
|
||||||
|
|
||||||
|
def register(self, client):
|
||||||
|
if client not in self.clients:
|
||||||
|
logging.info('registered client %s', client.peer)
|
||||||
|
self.clients.append(client)
|
||||||
|
|
||||||
|
def unregister(self, client):
|
||||||
|
if client in self.clients:
|
||||||
|
logging.info('unregistered client %s', client.peer)
|
||||||
|
self.clients.remove(client)
|
||||||
|
|
||||||
|
def broadcast(self, msg):
|
||||||
|
logging.debug('broadcasting message to: %s', self.clients)
|
||||||
|
for c in self.clients:
|
||||||
|
c.sendMessage(msg.encode('utf8'))
|
||||||
|
logging.debug('message sent to %s', c.peer)
|
||||||
|
|
||||||
|
#
|
||||||
|
# STATIC WEBSERVER
|
||||||
|
#
|
||||||
|
class web_server(Resource):
|
||||||
|
isLeaf = True
|
||||||
|
def render_GET(self, request):
|
||||||
|
logging.info('static website requested: %s', request)
|
||||||
|
if request.uri == '/':
|
||||||
|
return index_html
|
||||||
|
else:
|
||||||
|
return 'Bad request'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig(level=logging.INFO,handlers=[logging.FileHandler(PATH + 'logfile.log'),logging.StreamHandler()])
|
||||||
|
|
||||||
|
logging.info('web_tables.py starting up')
|
||||||
|
|
||||||
|
# Download alias files
|
||||||
|
result = try_download(PATH, 'peer_ids.csv', PEER_URL, (FILE_RELOAD * 86400))
|
||||||
|
logging.info(result)
|
||||||
|
|
||||||
|
result = try_download(PATH, 'subscriber_ids.csv', SUBSCRIBER_URL, (FILE_RELOAD * 86400))
|
||||||
|
logging.info(result)
|
||||||
|
|
||||||
|
# Make Alias Dictionaries
|
||||||
|
peer_ids = mk_full_id_dict(PATH, PEER_FILE, 'peer')
|
||||||
|
if peer_ids:
|
||||||
|
logging.info('ID ALIAS MAPPER: peer_ids dictionary is available')
|
||||||
|
|
||||||
|
subscriber_ids = mk_full_id_dict(PATH, SUBSCRIBER_FILE, 'subscriber')
|
||||||
|
if subscriber_ids:
|
||||||
|
logging.info('ID ALIAS MAPPER: subscriber_ids dictionary is available')
|
||||||
|
|
||||||
|
talkgroup_ids = mk_full_id_dict(PATH, TGID_FILE, 'tgid')
|
||||||
|
if talkgroup_ids:
|
||||||
|
logging.info('ID ALIAS MAPPER: talkgroup_ids dictionary is available')
|
||||||
|
|
||||||
|
local_subscriber_ids = mk_full_id_dict(PATH, LOCAL_SUB_FILE, 'subscriber')
|
||||||
|
if local_subscriber_ids:
|
||||||
|
logging.info('ID ALIAS MAPPER: local_subscriber_ids added to subscriber_ids dictionary')
|
||||||
|
subscriber_ids.update(local_subscriber_ids)
|
||||||
|
|
||||||
|
local_peer_ids = mk_full_id_dict(PATH, LOCAL_PEER_FILE, 'peer')
|
||||||
|
if local_peer_ids:
|
||||||
|
logging.info('ID ALIAS MAPPER: local_peer_ids added peer_ids dictionary')
|
||||||
|
peer_ids.update(local_peer_ids)
|
||||||
|
|
||||||
|
# Jinja2 Stuff
|
||||||
|
env = Environment(
|
||||||
|
loader=PackageLoader('web_tables', 'templates'),
|
||||||
|
autoescape=select_autoescape(['html', 'xml'])
|
||||||
|
)
|
||||||
|
|
||||||
|
dtemplate = env.get_template('hblink_table.html')
|
||||||
|
btemplate = env.get_template('bridge_table.html')
|
||||||
|
|
||||||
|
# Create Static Website index file
|
||||||
|
index_html = get_template(PATH + 'index_template.html')
|
||||||
|
index_html = index_html.replace('<<<system_name>>>', REPORT_NAME)
|
||||||
|
|
||||||
|
# Start update loop
|
||||||
|
update_stats = task.LoopingCall(build_stats)
|
||||||
|
update_stats.start(FREQUENCY)
|
||||||
|
|
||||||
|
# Connect to HBlink
|
||||||
|
reactor.connectTCP(HBLINK_IP, HBLINK_PORT, reportClientFactory())
|
||||||
|
|
||||||
|
# Create websocket server to push content to clients
|
||||||
|
dashboard_server = dashboardFactory('ws://*:9000')
|
||||||
|
dashboard_server.protocol = dashboard
|
||||||
|
reactor.listenTCP(9000, dashboard_server)
|
||||||
|
|
||||||
|
# Create static web server to push initial index.html
|
||||||
|
website = Site(web_server())
|
||||||
|
reactor.listenTCP(WEB_SERVER_PORT, website)
|
||||||
|
|
||||||
|
reactor.run()
|
Loading…
Reference in New Issue
Block a user