├── src ├── __init__.py ├── static │ └── stylesheet.css ├── Config.py ├── cbf_sensors_dash.py └── sensor_poll.py ├── dashboard.png ├── .travis.yml ├── DockerfileSensorPoll ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── .gitignore └── debug └── poll.py /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ska-sa/CBF-System-Dashboard/master/dashboard.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | sudo: required 3 | 4 | services: 5 | - docker 6 | notifications: 7 | email: false 8 | 9 | install: 10 | - make bootstrap 11 | 12 | script: 13 | - docker ps -a 14 | -------------------------------------------------------------------------------- /DockerfileSensorPoll: -------------------------------------------------------------------------------- 1 | # Usage 2 | # docker run -d -v ${PWD}/json_dumps/:/usr/src/json_dumps --name sensor-poll sensor-poll 3 | 4 | FROM python:2 5 | LABEL maintainer="Mpho Mphego " 6 | 7 | ENV DEBIAN_FRONTEND noninteractive 8 | WORKDIR /usr/src/apps 9 | 10 | # Install Python dependencies 11 | RUN pip install --no-cache-dir -U argcomplete \ 12 | coloredlogs \ 13 | context.api \ 14 | katcp 15 | 16 | # User data directory, containing Python scripts, config and etc. 17 | COPY src/sensor_poll.py /usr/src/apps/ 18 | RUN chmod +x /usr/src/apps/sensor_poll.py 19 | ENTRYPOINT ["/usr/src/apps/sensor_poll.py"] 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Usage 2 | # docker run -d -p 8888:8888 -v ${PWD}/json_dumps/:/usr/src/json_dumps --name cbf-sensor-dash cbf-sensor-dash 3 | 4 | FROM python:2 5 | LABEL maintainer="Mpho Mphego " 6 | 7 | ENV DEBIAN_FRONTEND noninteractive 8 | WORKDIR /usr/src/apps 9 | # Install Python dependencies 10 | RUN pip install --no-cache-dir -U argcomplete \ 11 | coloredlogs \ 12 | dash-core-components \ 13 | dash-html-components \ 14 | dash-renderer \ 15 | dash \ 16 | plotly 17 | 18 | # User data directory, containing Python scripts, config and etc. 19 | COPY src/Config.py /usr/src/apps/ 20 | COPY src/cbf_sensors_dash.py /usr/src/apps/ 21 | RUN chmod +x /usr/src/apps/cbf_sensors_dash.py 22 | ENTRYPOINT ["/usr/src/apps/cbf_sensors_dash.py"] 23 | -------------------------------------------------------------------------------- /src/static/stylesheet.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | width:1920px; 4 | background-color: 'white'; 5 | height:1080px; 6 | } 7 | /* Adapt to screen size*/ 8 | /* 9 | @media screen and (min-width: 500px) { 10 | body { 11 | width:420px; 12 | } 13 | } 14 | 15 | @media screen and (min-width: 800px) { 16 | body { 17 | width:720px; 18 | } 19 | } 20 | */ 21 | 22 | 23 | .btn-xl { 24 | /*Comment width for text to fit button*/ 25 | width: 100px; 26 | display:inline; 27 | /*display: inline-block;*/ 28 | height: 20px; 29 | line-height: 1.3; 30 | text-align: center; 31 | font-size: 10px; 32 | 33 | } 34 | /* 35 | .btn-vert { 36 | width: 20px; 37 | height: 500px; 38 | background-color: #ffffff; 39 | display: inline-block; 40 | }*/ 41 | .horizontal{ 42 | display: inline-block; 43 | width: 2px; 44 | /*text-align: center;*/ 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/Config.py: -------------------------------------------------------------------------------- 1 | 2 | title = "CBF Sensors Dashboard" 3 | metadata = 'charset="UTF-8" http-equiv="refresh" content="5"' 4 | # JS links 5 | js_link = { 6 | "codepen": 'https://codepen.io/mmphego/pen/KoJoZq.js', 7 | "jquery": "http://code.jquery.com/jquery-1.10.1.min.js" 8 | } 9 | # CSS links 10 | css_link = 'https://codepen.io/mmphego/pen/KoJoZq.css' 11 | 12 | # Style colors 13 | COLORS = [ 14 | { # NOMINAL 15 | 'background': 'green', 16 | 'color': 'white', 17 | }, 18 | { 19 | # WARN 20 | 'background': 'orange', 21 | 'color': 'white', 22 | }, 23 | { 24 | # ERROR 25 | 'background': 'red', 26 | 'color': 'white', 27 | }, 28 | { 29 | # FAILURE 30 | 'background': 'white', 31 | 'color': 'red', 32 | }, 33 | { 34 | # Other 35 | 'background': 'white', 36 | 'color': 'black', 37 | }, 38 | ] 39 | 40 | refresh_time = 10000 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SKA Africa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: bootstrap help build run bootstrap start stop clean logs_dash logs_sensors shell 2 | 3 | help: 4 | @echo "Please use \`make ' where is one of" 5 | @echo " bootstrap One-liner to make everything work!!!" 6 | @echo " build to build a docker container, configure CBF-Sensor-Dash" 7 | @echo " run to run pre-built CBF-Sensor-Dash container" 8 | @echo " start to start an existing CBF-Sensor-Dash container" 9 | @echo " stop to stop an existing CBF-Sensor-Dash container" 10 | @echo " logs to see the logs of a running container" 11 | @echo " shell to execute a shell on CBF-Sensor-Dash container" 12 | @echo " clean to stop and delete CBF-Sensor-Dash container" 13 | 14 | build: 15 | @docker build -t cbf-sensor-dash -f Dockerfile . 16 | @docker build -t sensor-poll -f DockerfileSensorPoll . 17 | 18 | run: 19 | @docker run -d -p 8888:8888 -v ${PWD}/json_dumps/:/usr/src/json_dumps --name cbf-sensor-dash cbf-sensor-dash 20 | @docker run -d -v ${PWD}/json_dumps/:/usr/src/json_dumps --name sensor-poll sensor-poll 21 | 22 | 23 | bootstrap: build run 24 | 25 | start: 26 | @docker start cbf-sensor-dash || true 27 | @docker start sensor-poll || true 28 | 29 | stop: 30 | @docker stop cbf-sensor-dash || true 31 | @docker stop sensor-poll || true 32 | 33 | clean: stop 34 | @docker rm -v cbf-sensor-dash || true 35 | @docker rmi cbf-sensor-dash || true 36 | @docker rm -v sensor-poll || true 37 | @docker rmi sensor-poll || true 38 | 39 | logs_dash: 40 | @docker logs -f cbf-sensor-dash 41 | 42 | logs_sensors: 43 | @docker logs -f sensor-poll 44 | 45 | shell: 46 | @docker exec -it cbf-sensor-dash /bin/bash 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CBF-System-Dashboard 2 | 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/087b7fe788d4415c8369dc0e365dff4c)](https://app.codacy.com/app/mmphego/CBF-System-Dashboard?utm_source=github.com&utm_medium=referral&utm_content=ska-sa/CBF-System-Dashboard&utm_campaign=Badge_Grade_Settings) 4 | [![Build Status](https://travis-ci.org/ska-sa/CBF-System-Dashboard.svg?branch=master)](https://travis-ci.org/ska-sa/CBF-System-Dashboard) 5 | [![LICENSE](https://img.shields.io/github/license/ska-sa/cbf-system-dashboard.svg?style=flat)](LICENSE) 6 | 7 | Simplified Docker based, CBF Sensor dashboard which polls the CBF sensors every x seconds and uses [Dash](https://plot.ly/dash) for its front-end. 8 | 9 | ![cbfdash](dashboard.png) 10 | 11 | 12 | ## What is Dash 13 | 14 | **Dash is a Python framework for building analytical web applications. No JavaScript/HTML required.** 15 | 16 | Build on top of Plotly.js, React, and Flask, Dash ties modern UI elements like dropdowns, sliders, and graphs directly to your analytical python code. 17 | 18 | To learn more about Dash, read the [extensive announcement letter](https://medium.com/@plotlygraphs/introducing-dash-5ecf7191b503) or [jump in with the user guide](https://plot.ly/dash). 19 | 20 | View the [Dash User Guide](https://plot.ly/dash). It's chock-full of examples, pro tips, and guiding principles. 21 | 22 | More info visit: [Dash](https://github.com/plotly/dash) 23 | 24 | ### Usage 25 | 26 | #### Build 27 | 28 | Build both the front-end and back-end docker images. 29 | 30 | ```shell 31 | make bootstrap 32 | ``` 33 | 34 | #### Run 35 | 36 | ```shell 37 | make run 38 | ``` 39 | 40 | ## Feedback 41 | 42 | Feel free to fork it or send me PR to improve it. 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /debug/poll.py: -------------------------------------------------------------------------------- 1 | ## DEBUGGING SCRIPT 2 | 3 | import logging 4 | import os 5 | import sys 6 | import time 7 | import katcp 8 | import argcomplete 9 | import argparse 10 | import atexit 11 | import coloredlogs 12 | import functools 13 | import gc 14 | import json 15 | import katcp 16 | import logging 17 | import os 18 | import sys 19 | import time 20 | import threading 21 | import signal 22 | 23 | from collections import OrderedDict 24 | from itertools import izip_longest 25 | from pprint import PrettyPrinter 26 | 27 | from concurrent.futures import TimeoutError 28 | 29 | from katcp import ioloop_manager, resource_client 30 | from katcp.core import ProtocolFlags 31 | from katcp.resource_client import KATCPSensorError 32 | 33 | class SensorPoll(object): 34 | def __init__(self, katcp_client="127.0.0.1:7147", array_name='array0', timeout=30): 35 | self.katcp_client_ip, self.katcp_client_port = katcp_client.split(":") 36 | self.array_name = array_name 37 | self._timeout = timeout 38 | self._katcp_rct = None 39 | self._katcp_rct_sensor = None 40 | self._rct = None 41 | 42 | @property 43 | def rct(self): 44 | if self._rct is not None: 45 | return self._rct 46 | else: 47 | self.io_manager = ioloop_manager.IOLoopManager() 48 | self.io_wrapper = resource_client.IOLoopThreadWrapper(self.io_manager.get_ioloop()) 49 | atexit.register(self.io_manager.stop) 50 | self.io_wrapper.default_timeout = self._timeout 51 | self.io_manager.start() 52 | self.io_manager.setDaemon =True 53 | self.rc = resource_client.KATCPClientResource( 54 | dict( 55 | name="{}".format(self.katcp_client_ip), 56 | address=("{}".format(self.katcp_client_ip), self.katcp_client_port), 57 | controlled=True, 58 | ) 59 | ) 60 | self.rc.set_ioloop(self.io_manager.get_ioloop()) 61 | self._rct = resource_client.ThreadSafeKATCPClientResourceWrapper( 62 | self.rc, self.io_wrapper) 63 | self._rct.start() 64 | self._rct.setDaemon = True 65 | atexit.register(self._rct.stop) 66 | try: 67 | self._rct.until_synced(timeout=self._timeout) 68 | except Exception: 69 | self._rct.stop() 70 | self._rct.join() 71 | return self._rct 72 | 73 | 74 | @property 75 | def katcp_rct_sensor(self): 76 | if self._katcp_rct_sensor is None: 77 | try: 78 | katcp_prot = "5,0,M" 79 | _major, _minor, _flags = katcp_prot.split(",") 80 | protocol_flags = ProtocolFlags(int(_major), int(_minor), _flags) 81 | assert hasattr(self.rct, 'req') 82 | assert hasattr(self.rct.req, 'array_list') 83 | reply, informs = self.rct.req.array_list(self.array_name) 84 | assert reply.reply_ok() 85 | assert informs[0].arguments > 1 86 | self.katcp_array_port, self.katcp_sensor_port = ([ 87 | int(i) for i in informs[0].arguments[1].split(",") 88 | ]) 89 | except Exception: 90 | raise NotImplementedError 91 | else: 92 | katcp_rc = resource_client.KATCPClientResource( 93 | dict( 94 | name="{}".format(self.katcp_client_ip), 95 | address=( 96 | "{}".format(self.katcp_client_ip), 97 | "{}".format(self.katcp_sensor_port), 98 | ), 99 | preset_protocol_flags=protocol_flags, 100 | controlled=True, 101 | ) 102 | ) 103 | katcp_rc.set_ioloop(self.io_manager.get_ioloop()) 104 | self._katcp_rct_sensor = resource_client.ThreadSafeKATCPClientResourceWrapper( 105 | katcp_rc, self.io_wrapper) 106 | 107 | self._katcp_rct_sensor.start() 108 | atexit.register(self._katcp_rct_sensor.start) 109 | try: 110 | self._katcp_rct_sensor.until_synced(timeout=self._timeout) 111 | except Exception: 112 | self._katcp_rct_sensor.stop() 113 | self._katcp_rct_sensor.join() 114 | else: 115 | return self._katcp_rct_sensor 116 | else: 117 | if not self._katcp_rct_sensor.is_active(): 118 | self._katcp_rct_sensor.start() 119 | atexit.register(self._katcp_rct_sensor.stop) 120 | try: 121 | time.sleep(1) 122 | self._katcp_rct_sensor.until_synced(timeout=self._timeout) 123 | return self._katcp_rct_sensor 124 | except Exception: 125 | self._katcp_rct_sensor.stop() 126 | self._katcp_rct_sensor.join() 127 | else: 128 | try: 129 | assert hasattr(self._katcp_rct_sensor, "req"), 'sensors rct not running' 130 | assert hasattr(self._katcp_rct_sensor.req, "sensor_value"), 'no sensors on sensors rct' 131 | return self._katcp_rct_sensor 132 | except AssertionError: 133 | del self._katcp_rct_sensor 134 | self._katcp_rct_sensor = None 135 | return self._katcp_rct_sensor 136 | 137 | 138 | def get_sensors(self): 139 | try: 140 | assert hasattr(self.katcp_rct_sensor, 'req') 141 | assert hasattr(self.katcp_rct_sensor.req, 'sensor_value') 142 | reply, informs = self.katcp_rct_sensor.req.sensor_value() 143 | assert reply.reply_ok() 144 | except Exception: 145 | raise NotImplementedError('Not Implemented') 146 | else: 147 | return informs 148 | 149 | 150 | if __name__ == '__main__': 151 | sensors = SensorPoll() 152 | try: 153 | while True: 154 | print ("Waiting") 155 | print (sensors.get_sensors()) 156 | time.sleep(5) 157 | except KeyboardInterrupt: 158 | import IPython; globals().update(locals()); IPython.embed(header='Python Debugger') -------------------------------------------------------------------------------- /src/cbf_sensors_dash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import fcntl 5 | import glob 6 | import json 7 | import logging 8 | import os 9 | import socket 10 | import struct 11 | import sys 12 | import time 13 | import types 14 | import urllib2 15 | from collections import OrderedDict 16 | from pprint import PrettyPrinter 17 | 18 | import argcomplete 19 | import coloredlogs 20 | import dash 21 | import dash_core_components as dcc 22 | import dash_html_components as html 23 | # import flask 24 | from dash.dependencies import Event, Input, Output 25 | from flask import send_from_directory 26 | 27 | import Config 28 | 29 | pp = PrettyPrinter(indent=4) 30 | log_level = None 31 | log_format = "%(asctime)s - %(name)s:%(process)d - %(levelname)s - %(module)s - %(pathname)s : %(lineno)d - %(message)s" 32 | 33 | parser = argparse.ArgumentParser(description="Should probably put the description here!") 34 | parser.add_argument( 35 | "-i", 36 | "--interface", 37 | dest="interface", 38 | action="store", 39 | default="eth0", 40 | help="network interface [Default: eth0]", 41 | ) 42 | parser.add_argument( 43 | "-p", 44 | "--port", 45 | dest="port", 46 | action="store_true", 47 | default=8888, 48 | help="flask port [Default: 8888]", 49 | ) 50 | parser.add_argument( 51 | "--nodebug", 52 | dest="debug", 53 | action="store_false", 54 | default=True, 55 | help="flask with no debug [Default: False]", 56 | ) 57 | parser.add_argument( 58 | "--nothread", 59 | dest="threaded", 60 | action="store_false", 61 | default=True, 62 | help="flask with threading [Default: False]", 63 | ) 64 | parser.add_argument( 65 | "--path", 66 | dest="sensor_path", 67 | action="store", 68 | default=None, 69 | help="path to where the sensor data .json file is!", 70 | ) 71 | parser.add_argument( 72 | "--loglevel", 73 | dest="log_level", 74 | action="store", 75 | default="INFO", 76 | help="log level to use, default INFO, options INFO, DEBUG, ERROR", 77 | ) 78 | 79 | argcomplete.autocomplete(parser) 80 | args = vars(parser.parse_args()) 81 | 82 | 83 | def set_style(state): 84 | """ 85 | Set html/css style according to sensor state 86 | Params 87 | ====== 88 | state: str 89 | sensor state, eg: nominal, warn, error and other 90 | 91 | Return 92 | ====== 93 | style: dict 94 | dictionary containing html/css style 95 | """ 96 | 97 | style = {} 98 | state = state.lower() 99 | if state: 100 | if state == "nominal": 101 | style = { 102 | "backgroundColor": Config.COLORS[0]["background"], 103 | "color": Config.COLORS[0]["color"], 104 | "display": "inline-block", 105 | } 106 | elif state == "warn": 107 | style = { 108 | "backgroundColor": Config.COLORS[1]["background"], 109 | "color": Config.COLORS[1]["color"], 110 | "display": "inline-block", 111 | } 112 | elif state == "error": 113 | style = { 114 | "backgroundColor": Config.COLORS[2]["background"], 115 | "color": Config.COLORS[2]["color"], 116 | "display": "inline-block", 117 | } 118 | elif state == "failure": 119 | style = { 120 | "backgroundColor": Config.COLORS[3]["background"], 121 | "color": Config.COLORS[3]["color"], 122 | "font-weight": "bold", 123 | "font-style": "italic", 124 | "display": "inline-block", 125 | } 126 | else: 127 | style = { 128 | "backgroundColor": Config.COLORS[4]["background"], 129 | "color": Config.COLORS[4]["color"], 130 | "display": "inline-block", 131 | } 132 | return style 133 | 134 | 135 | def add_buttons(child, _id, _status): 136 | """ 137 | Params 138 | ====== 139 | 140 | Return 141 | ====== 142 | 143 | """ 144 | # Button click redirection -- https://github.com/plotly/dash-html-components/issues/16 145 | _button = [ 146 | html.Button( 147 | children=child, 148 | style=set_style(_status), 149 | type="button", 150 | className="btn-xl", 151 | id=_id, 152 | n_clicks=0, 153 | ) 154 | ] 155 | # if '-020' in child: 156 | # return html.A(_button, id='button', href='/page-2') 157 | # else: 158 | _button.append(html.Hr(className="horizontal")) 159 | return html.A(_button, id="button", href="/page-2") 160 | 161 | 162 | def generate_line(host): 163 | """ 164 | Params 165 | ====== 166 | 167 | Return 168 | ====== 169 | 170 | """ 171 | return [ 172 | add_buttons(i[0], "id_%s_%s" % (host, _c), i[-1]) 173 | for _c, i in enumerate(sensor_format.get(host)) 174 | ] 175 | 176 | 177 | def generate_table(): 178 | """ 179 | Params 180 | ====== 181 | 182 | Return 183 | ====== 184 | 185 | """ 186 | return [ 187 | html.Div( 188 | [html.Span(children=i, style={"display": "inline-block"}) for i in generate_line(x)] 189 | ) 190 | for x in sorted(sensor_format.keys()) 191 | ] 192 | 193 | 194 | def get_ip_address(ifname): 195 | """ 196 | Get current IP address of a network interface card 197 | 198 | Params 199 | ====== 200 | ifname: str 201 | Interface name eg: eth0 202 | Return 203 | ====== 204 | IP: str 205 | Current IP of the interface 206 | """ 207 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 208 | return socket.inet_ntoa( 209 | fcntl.ioctl(s.fileno(), 0x8915, struct.pack("256s", ifname[:15]))[20:24] # SIOCGIFADDR 210 | ) 211 | 212 | 213 | def file_exists(url): 214 | """ 215 | Check if file in url exists 216 | 217 | Params 218 | ====== 219 | url: str 220 | http(s):// link 221 | Return 222 | ====== 223 | Boolean 224 | """ 225 | try: 226 | request = urllib2.Request(url) 227 | request.get_method = lambda: "HEAD" 228 | _ = urllib2.urlopen(request) 229 | return True 230 | except Exception: 231 | return False 232 | 233 | 234 | def get_sensors(json_file): 235 | """ 236 | Read sensor values stored in a json file 237 | 238 | Params 239 | ====== 240 | json_file: str 241 | json path 242 | 243 | Return 244 | ====== 245 | data: dict 246 | json dump in a dict format 247 | """ 248 | logger.info("Reading latest sensor values from %s" % json_file) 249 | with open(json_file) as json_data: 250 | data = json.load(json_data) 251 | return data 252 | 253 | 254 | if args.get("log_level", "INFO"): 255 | log_level = args.get("log_level", "INFO").upper() 256 | try: 257 | logging.basicConfig(level=getattr(logging, log_level), format=log_format) 258 | logger = logging.getLogger(os.path.basename(sys.argv[0])) 259 | except AttributeError: 260 | raise RuntimeError("No such log level: %s" % log_level) 261 | else: 262 | if log_level == "DEBUG": 263 | coloredlogs.install(level=log_level, fmt=log_format) 264 | else: 265 | coloredlogs.install(level=log_level) 266 | 267 | if not args.get("sensor_path"): 268 | try: 269 | cur_path = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0] 270 | except NameError: 271 | cur_path = os.path.split(os.path.dirname(os.path.abspath(__name__)))[0] 272 | 273 | try: 274 | json_dumps_dir = os.path.join(cur_path + "/json_dumps") 275 | if not os.path.exists(json_dumps_dir): 276 | raise AssertionError() 277 | sensor_values_json = max( 278 | glob.iglob(json_dumps_dir + "/sensor_values.json"), key=os.path.getctime 279 | ) 280 | except AssertionError: 281 | logger.error("No json dump file. Exiting!!!") 282 | sys.exit(1) 283 | else: 284 | sensor_values_json = args.get("sensor_path") 285 | 286 | try: 287 | ordered_sensor_dict = get_sensors(json_dumps_dir + "/ordered_sensor_values.json") 288 | except Exception: 289 | ordered_sensor_dict = {} 290 | 291 | sensor_format = get_sensors(sensor_values_json) 292 | host = get_ip_address(args.get("interface")) 293 | 294 | title = Config.title 295 | refresh_time = int(Config.refresh_time) 296 | 297 | app = dash.Dash(name=title) 298 | try: 299 | css_link = Config.css_link 300 | logger.info("Loading css/js from URL: %s" % css_link) 301 | if not file_exists(css_link): 302 | raise AssertionError() 303 | app.css.config.serve_locally = False 304 | app.scripts.config.serve_locally = False 305 | except AssertionError: 306 | logger.info("Loading local css/js files") 307 | app.css.config.serve_locally = True 308 | app.scripts.config.serve_locally = True 309 | else: 310 | app.css.append_css({"external_url": css_link}) 311 | 312 | # Monkey patching 313 | app.title = types.StringType(title) 314 | # metadata = Config.metadata 315 | # app.meta = types.StringType(metadata) 316 | 317 | # HTML Layout 318 | html_layout = html.Div( 319 | [ 320 | html.H3("Last Updated: %s" % time.ctime(), style={"margin": 0, "color": "green"}), 321 | html.Div(generate_table()), 322 | ] 323 | ) 324 | 325 | app.layout = html.Div( 326 | [ 327 | html.Link(rel="stylesheet", href="/static/stylesheet.css"), 328 | html.Div( 329 | [ 330 | # Each "page" will modify this element 331 | html.Div(id="content-container"), 332 | # This Location component represents the URL bar 333 | dcc.Location(id="url", refresh=False), 334 | ] 335 | ), 336 | html.Div([dcc.Interval(id="refresh", interval=refresh_time), html.Div(id="content")]), 337 | ] 338 | ) 339 | 340 | # Update the `content` div with the `layout` object. 341 | # When you save this file, `debug=True` will re-run this script, serving the new layout 342 | @app.callback(Output("content", "children"), events=[Event("refresh", "interval")]) 343 | def display_layout(): 344 | return html_layout 345 | 346 | 347 | @app.server.route("/static/") 348 | def static_file(path): 349 | static_folder = os.path.join(os.getcwd(), "static") 350 | logger.info("Loaded css/js from %s" % static_folder) 351 | return send_from_directory(static_folder, path) 352 | 353 | 354 | @app.callback(Output("content-container", "children"), [Input("url", "pathname")]) 355 | def display_page(pathname): 356 | # https://stackoverflow.com/questions/43981275/index-json-files-in-elasticsearch-using-python#43982859 357 | # https://stackoverflow.com/questions/44053745/querying-elasticsearch-with-python-requests 358 | # https://stackoverflow.com/questions/27189892/how-to-filter-json-array-in-python#27190305 359 | # https://stackoverflow.com/questions/43371547/elasticsearch-python-client-indexing-json 360 | # https://www.logicalfeed.com/posts/1182/upload-bulk-json-data-to-elasticsearch-using-python 361 | if pathname == "/": 362 | # return html_layout 363 | pass 364 | elif pathname == "/page-2": 365 | try: 366 | _sensors = json.dumps( 367 | OrderedDict(ordered_sensor_dict), indent=4, sort_keys=True, separators=(",", ": ") 368 | ) 369 | except: 370 | _sensors = json.dumps( 371 | OrderedDict(sensor_format), indent=4, sort_keys=True, separators=(",", ": ") 372 | ) 373 | 374 | return html.Div([dcc.Link(html.Pre(_sensors), href="/"), html.Br()]) 375 | else: 376 | return html.Div( 377 | [ 378 | html.A( 379 | "I guess this is like a 404 - no content available. Click to Go Home", href="/" 380 | ) 381 | ] 382 | ) 383 | 384 | 385 | if __name__ == "__main__": 386 | app.run_server( 387 | host=host, 388 | port=args.get("port"), 389 | debug=args.get("debug"), 390 | extra_files=[sensor_values_json], 391 | threaded=args.get("threaded"), 392 | ) 393 | -------------------------------------------------------------------------------- /src/sensor_poll.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argcomplete 4 | import argparse 5 | import atexit 6 | import coloredlogs 7 | import functools 8 | import gc 9 | import json 10 | import katcp 11 | import logging 12 | import os 13 | import ipaddress 14 | import socket 15 | import sys 16 | import time 17 | import threading 18 | 19 | from collections import OrderedDict 20 | from itertools import izip_longest 21 | from ast import literal_eval as evaluate 22 | from pprint import PrettyPrinter 23 | 24 | 25 | def retry(func, count=20, wait_time=300): 26 | @functools.wraps(func) 27 | def wrapper(*args, **kwargs): 28 | retExc = TypeError 29 | for _ in xrange(count): 30 | while True: 31 | try: 32 | return func(*args, **kwargs) 33 | except Exception as exc: 34 | retExc = exc 35 | time.sleep(wait_time) 36 | continue 37 | break 38 | raise retExc 39 | return wrapper 40 | 41 | 42 | def combined_Dict_List(*args): 43 | """ 44 | Combining two/more dictionaries into one with the same keys 45 | 46 | Params 47 | ======= 48 | args: list 49 | list of dicts to combine 50 | 51 | Return 52 | ======= 53 | result: dict 54 | combined dictionaries 55 | """ 56 | result = {} 57 | for _dict in args: 58 | for key in result.viewkeys() | _dict.keys(): 59 | if key in _dict: 60 | result.setdefault(key, []).extend([_dict[key]]) 61 | return result 62 | 63 | 64 | # This class could be imported from a utility module 65 | class LoggingClass(object): 66 | @property 67 | def logger(self): 68 | log_format = "%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(pathname)s : %(lineno)d - %(message)s" 69 | name = ".".join([os.path.basename(sys.argv[0]), self.__class__.__name__]) 70 | logging.basicConfig(format=log_format) 71 | coloredlogs.install(fmt=log_format) 72 | return logging.getLogger(name) 73 | 74 | 75 | class SensorPoll(LoggingClass): 76 | def __init__(self, katcp_ip=None, katcp_port=7147): 77 | """ 78 | Parameters 79 | ========= 80 | katcp_ip: str 81 | IP to connect to! 82 | katcp_port: int 83 | Port to connect to! [Defaults: 7147] 84 | """ 85 | 86 | try: 87 | assert katcp_ip 88 | self.katcp_ip = katcp_ip 89 | ipaddress.ip_address(u"{}".format(self.katcp_ip)) 90 | self.hostname = ''.join(socket.gethostbyaddr(katcp_ip)[1]) 91 | except Exception: 92 | self.logger.exception("Invalid KATCP_IP!") 93 | raise 94 | self.katcp_port = katcp_port 95 | self._kcp_connect() 96 | 97 | @retry 98 | def _kcp_connect(self): 99 | try: 100 | self._started = False 101 | self.primary_client = self.katcp_request(which_port=self.katcp_port) 102 | atexit.register(self.cleanup, self.primary_client) 103 | assert isinstance(self.primary_client, katcp.client.BlockingClient) 104 | reply, informs = self.sensor_request(self.primary_client) 105 | assert reply.reply_ok() 106 | katcp_array_list = informs[0].arguments 107 | assert isinstance(katcp_array_list, list) 108 | self.katcp_array_port, self.katcp_sensor_port = [ 109 | int(i) for i in katcp_array_list[1].split(",") 110 | ] 111 | self.array_name = katcp_array_list[0] 112 | assert isinstance(self.katcp_array_port, int) 113 | assert isinstance(self.katcp_sensor_port, int) 114 | except Exception: 115 | self.logger.error( 116 | "No running array on {}:{}!!!!".format(self.katcp_ip, self.katcp_port), 117 | #exc_info=True 118 | ) 119 | if self.primary_client.is_connected(): 120 | self.cleanup(self.primary_client) 121 | raise 122 | else: 123 | if self._started: 124 | self._started = False 125 | self.sec_client = self.katcp_request(which_port=self.katcp_array_port) 126 | atexit.register(self.cleanup, self.sec_client) 127 | 128 | if self._started: 129 | self._started = False 130 | self.sec_sensors_katcp_con = self.katcp_request( 131 | which_port=self.katcp_sensor_port 132 | ) 133 | atexit.register(self.cleanup, self.sec_sensors_katcp_con) 134 | 135 | self.logger.info( 136 | "Katcp connection established: IP {}, Primary Port: {}, Array Port: {}, " 137 | "Sensor Port: {}".format( 138 | self.katcp_ip, 139 | self.katcp_port, 140 | self.katcp_array_port, 141 | self.katcp_sensor_port, 142 | ) 143 | ) 144 | time.sleep(1) 145 | try: 146 | self.input_mapping, self.hostname_mapping = self.do_mapping() 147 | except Exception: 148 | self.cleanup(self.sec_client) 149 | self.cleanup(self.sec_sensors_katcp_con) 150 | self.logger.error( 151 | "Ayeyeyeye! it broke cannot do mappings", 152 | # exc_info=True 153 | ) 154 | raise 155 | 156 | def katcp_request( 157 | self, which_port, katcprequest="array-list", katcprequestArg=None, timeout=10 158 | ): 159 | """ 160 | Katcp requests 161 | 162 | Parameters 163 | ========= 164 | which_port: str 165 | Katcp port to connect to! 166 | katcprequest: str 167 | Katcp requests messages [Defaults: 'array-list'] 168 | katcprequestArg: str 169 | katcp requests messages arguments eg. array-list array0 [Defaults: None] 170 | timeout: int 171 | katcp timeout [Defaults :10] 172 | 173 | Return 174 | ====== 175 | reply, informs : tuple 176 | katcp request messages 177 | """ 178 | # self._started = False 179 | if not self._started: 180 | self._started = True 181 | self.logger.info( 182 | "Establishing katcp connection on {}:{}".format( 183 | self.katcp_ip, which_port 184 | ) 185 | ) 186 | client = katcp.BlockingClient(self.katcp_ip, which_port) 187 | client.setDaemon(True) 188 | client.start() 189 | time.sleep(0.1) 190 | try: 191 | is_connected = client.wait_running(timeout) 192 | assert is_connected 193 | self.logger.info( 194 | "Katcp client connected to {}:{}\n".format( 195 | self.katcp_ip, which_port 196 | ) 197 | ) 198 | return client 199 | except Exception: 200 | client.stop() 201 | self.logger.error("Could not connect to katcp, timed out.") 202 | 203 | def sensor_request( 204 | self, client, katcprequest="array-list", katcprequestArg=None, timeout=10 205 | ): 206 | """ 207 | Katcp requests 208 | 209 | Parameters 210 | ========= 211 | client: katcp.client.BlockingClient 212 | katcp running client 213 | katcprequest: str 214 | Katcp requests messages [Defaults: 'array-list'] 215 | katcprequestArg: str 216 | katcp requests messages arguments eg. array-list array0 [Defaults: None] 217 | timeout: int 218 | katcp timeout [Defaults :10] 219 | 220 | Return 221 | ====== 222 | reply, informs : tuple 223 | katcp request messages 224 | """ 225 | try: 226 | if katcprequestArg: 227 | reply, informs = client.blocking_request( 228 | katcp.Message.request(katcprequest, katcprequestArg), 229 | timeout=timeout, 230 | ) 231 | else: 232 | reply, informs = client.blocking_request( 233 | katcp.Message.request(katcprequest), timeout=timeout 234 | ) 235 | assert reply.reply_ok() 236 | except Exception: 237 | self.logger.error("Failed to execute katcp command") 238 | # time.sleep(20) 239 | raise 240 | else: 241 | return reply, informs 242 | 243 | def cleanup(self, client): 244 | if self._started: 245 | self.logger.debug("Some Cleaning Up!!!") 246 | client.stop() 247 | time.sleep(0.1) 248 | if client.is_connected(): 249 | self.logger.error("Did not clean up client properly, %s" % client.bind_address) 250 | 251 | 252 | @property 253 | def get_sensor_values(self): 254 | try: 255 | assert self.katcp_sensor_port 256 | reply, informs = self.sensor_request( 257 | self.sec_sensors_katcp_con, katcprequest="sensor-value") 258 | assert reply.reply_ok() 259 | assert int(reply.arguments[-1]) 260 | yield [inform.arguments for inform in informs] 261 | except AssertionError: 262 | self.logger.error("No Sensors!!! Exiting!!!") 263 | raise 264 | 265 | @property 266 | def get_hostmapping(self): 267 | try: 268 | assert self.katcp_sensor_port 269 | reply, informs = self.sensor_request( 270 | self.sec_sensors_katcp_con, 271 | katcprequest="sensor-value", 272 | katcprequestArg="hostname-functional-mapping", 273 | ) 274 | assert reply.reply_ok() 275 | assert int(reply.arguments[-1]) 276 | yield [inform.arguments for inform in informs] 277 | except AssertionError: 278 | self.cleanup(self.sec_sensors_katcp_con) 279 | self.logger.error("No Sensors!!! Exiting!!!") 280 | raise 281 | 282 | @property 283 | def get_inputlabel(self, i=1): 284 | try: 285 | assert self.katcp_array_port 286 | for i in xrange(i): 287 | reply, informs = self.sensor_request( 288 | self.sec_client, 289 | katcprequest="sensor-value", 290 | katcprequestArg="input-labelling", 291 | ) 292 | assert reply.reply_ok() 293 | assert int(reply.arguments[-1]) 294 | yield [inform.arguments for inform in informs] 295 | except AssertionError: 296 | self.cleanup(self.sec_client) 297 | self.logger.error("No Sensors!!! Exiting!!!") 298 | raise 299 | 300 | @property 301 | def get_sensor_dict(self): 302 | sensor_value_informs = next(self.get_sensor_values) 303 | self.logger.debug("Converting sensor list to dict!!!") 304 | # sensors name + status + value 305 | self.original_sensors = dict( 306 | (x[0], x[1:]) for x in [i[2:] for i in sensor_value_informs] 307 | ) 308 | # sensors name and status 309 | simplified_sensors = dict( 310 | (x[0], x[1]) for x in [i[2:] for i in sensor_value_informs] 311 | ) 312 | simplified_ordered_sensors = OrderedDict(sorted(simplified_sensors.items())) 313 | try: 314 | return simplified_ordered_sensors 315 | except Exception as exc: 316 | raise exc 317 | 318 | def do_mapping(self): 319 | try: 320 | self.logger.debug("Mapping input labels and hostnames") 321 | hostname_mapping = next(self.get_hostmapping)[-1][-1] 322 | input_mapping = next(self.get_inputlabel)[-1][-1] 323 | except Exception: 324 | self.logger.error( 325 | "Serious error occurred, cannot continue!!! Missing sensors" 326 | ) 327 | raise 328 | else: 329 | input_mapping = dict(list(i)[0:3:2] for i in evaluate(input_mapping)) 330 | input_mapping = dict((v, k) for k, v in input_mapping.iteritems()) 331 | update_maps = [] 332 | for i in input_mapping.values(): 333 | if "_y" in i: 334 | i = i.replace("_y", "_xy") 335 | elif "_x" in i: 336 | i = i.replace("_x", "_xy") 337 | elif "v" in i: 338 | i = i.replace("v", "_vh") 339 | elif "h" in i: 340 | i = i.replace("h", "_vh") 341 | 342 | update_maps.append(i) 343 | 344 | update_maps = sorted(update_maps) 345 | input_mapping = dict(zip(sorted(input_mapping.keys()), update_maps)) 346 | hostname_mapping = dict( 347 | (v, k) for k, v in evaluate(hostname_mapping).iteritems() 348 | ) 349 | return [input_mapping, hostname_mapping] 350 | 351 | def get_list_index(self, String, List): 352 | """ 353 | Find the index of a string in a list 354 | 355 | Params 356 | ======= 357 | String: str 358 | String to search in list 359 | List: list 360 | List to be searched! 361 | 362 | Return 363 | ======= 364 | list 365 | """ 366 | try: 367 | return [_c for _c, i in enumerate(List) if any(String in x for x in i)][0] 368 | except Exception: 369 | self.logger.error("Failed to find the index of string in list") 370 | 371 | def new_mapping(self, _host): 372 | try: 373 | self.logger.debug("Sorting ordered sensor dict by %ss!!!" % _host) 374 | ordered_sensor_dict = self.get_sensor_dict 375 | assert isinstance(ordered_sensor_dict, OrderedDict) 376 | except Exception: 377 | self.logger.error("Failed to retrieve sensor dict", exc_info=True) 378 | else: 379 | mapping = [] 380 | for key, value in ordered_sensor_dict.iteritems(): 381 | key_s = key.split(".") 382 | host = key_s[0].lower() 383 | if host.startswith(_host) and ("device-status" in key_s): 384 | new_value = [x.replace("device-status", value) for x in key_s[1:]] 385 | if "network-reorder" in new_value: 386 | # rename such that, it fits on html/button 387 | _indices = new_value.index("network-reorder") 388 | new_value[_indices] = new_value[_indices].replace( 389 | "network-reorder", "Net-ReOrd" 390 | ) 391 | if "missing-pkts" in new_value: 392 | # rename such that, it fits on html/button 393 | _indices = new_value.index("missing-pkts") 394 | new_value[_indices] = new_value[_indices].replace( 395 | "missing-pkts", "hmcReOrd" 396 | ) 397 | if "bram-reorder" in new_value: 398 | # rename such that, it fits on html/button 399 | _indices = new_value.index("bram-reorder") 400 | new_value[_indices] = new_value[_indices].replace( 401 | "bram-reorder", "bramReOrd" 402 | ) 403 | 404 | new_dict = dict( 405 | izip_longest(*[iter([host, new_value])] * 2, fillvalue="") 406 | ) 407 | mapping.append(new_dict) 408 | 409 | new_mapping = combined_Dict_List(*mapping) 410 | for host, _list in new_mapping.iteritems(): 411 | if host in self.hostname_mapping: 412 | new_hostname = host.replace(_host, "") + self.hostname_mapping[ 413 | host 414 | ].replace("skarab", "-").replace("-01", "") 415 | _ = [value.insert(0, new_hostname) 416 | for value in _list if len(value) == 1] 417 | 418 | return new_mapping 419 | 420 | @property 421 | def map_xhost_sensors(self): 422 | """ 423 | Needs to be in this format: 424 | 'host03': [ 425 | ['03-020308', 'warn'], 426 | ['network', 'warn'], 427 | ['spead-rx', 'nominal'], 428 | ['Net-ReOrd', 'nominal'], 429 | ['hmcReOrd', 'warn'], 430 | ['bram-reorder', 'error'], 431 | ['vacc', 'error'], 432 | ['spead-tx', 'nominal'] 433 | ] 434 | } 435 | 436 | """ 437 | xhost_sig_chain = [ 438 | "-02", 439 | "network", 440 | "spead-rx", 441 | "Net-ReOrd", 442 | "hmcReOrd", 443 | "bramReOrd", 444 | "vacc", 445 | "spead-tx", 446 | ] 447 | try: 448 | new_mapping = self.new_mapping("xhost") 449 | assert isinstance(new_mapping, dict) 450 | except Exception: 451 | self.logger.error("Failed to map xhosts", exc_info=True) 452 | else: 453 | new_dict_mapping = {} 454 | for keys, values in new_mapping.iteritems(): 455 | keys_ = keys[1:] 456 | new_dict_mapping[keys_] = [] 457 | for value in values: 458 | if (len(value) <= 2) and (not value[0].startswith("xeng")): 459 | new_dict_mapping[keys_].append(value) 460 | if ( 461 | value[0].startswith("xeng") 462 | and value not in new_dict_mapping.values() 463 | ): 464 | if "vacc" in value: 465 | new_dict_mapping[keys_].append(value[1:]) 466 | if "spead-tx" in value: 467 | new_dict_mapping[keys_].append(value[1:]) 468 | if "bramReOrd" in value: 469 | new_dict_mapping[keys_].append(value[1:]) 470 | 471 | # _ = [listA.insert(_index, listA.pop(self.get_list_index(_sig, listA))) 472 | # for _, listA in new_dict_mapping.iteritems() for _index, _sig in enumerate(xhost_sig_chain)] 473 | # return new_dict_mapping 474 | fixed_dict_mapping = {} 475 | for host_, listA in new_dict_mapping.iteritems(): 476 | listA = listA[: len(xhost_sig_chain)] 477 | for _index, _sig in enumerate(xhost_sig_chain): 478 | listA.insert(_index, listA.pop(self.get_list_index(_sig, listA))) 479 | fixed_dict_mapping[host_] = listA 480 | return fixed_dict_mapping 481 | 482 | @property 483 | def map_fhost_sensors(self): 484 | """ 485 | { 486 | 'fhost03': [ 487 | ['SKA-020709', 'warn'], 488 | ['fhost00', 'skarab020709-01'], 489 | ['ant0_y', 'inputlabel'], 490 | ['network', 'nominal'], 491 | ['spead-rx', 'failure'], 492 | ['Net-ReOrd', 'nominal'], 493 | ['cd', 'warn'], 494 | ['pfb', 'warn'], 495 | ['ct', 'nominal'], 496 | ['spead-tx', 'nominal'], 497 | ['->XEngine', 'xhost'] 498 | ] 499 | } 500 | 501 | """ 502 | 503 | # Abbreviated signal chain 504 | # F_LRU -> Host -> input_label -> network-trx -> spead-rx -> network-reorder -> cd -> pfb -->> 505 | # -->> ct -> spead-tx -> network-trx : [To Xengine ] 506 | # issue reading cmc3 input labels 507 | # fhost_sig_chain = ['SKA', 'fhost', 'network', 'spead-rx', 'Net-ReOrd', 'cd', 'pfb', 508 | fhost_sig_chain = [ 509 | "-02", 510 | "input", 511 | "network", 512 | "spead-rx", 513 | "Net-ReOrd", 514 | "cd", 515 | "pfb", 516 | "ct", 517 | "spead-tx", 518 | ] 519 | try: 520 | new_mapping = self.new_mapping("fhost") 521 | assert isinstance(new_mapping, dict) 522 | except Exception: 523 | self.logger.error("Failed to map fhosts", exc_info=True) 524 | else: 525 | for host, values in new_mapping.iteritems(): 526 | if host in self.hostname_mapping: 527 | values.insert( 528 | 2, [self.input_mapping[self.hostname_mapping[host]], "inputlabel"] 529 | ) 530 | # values.append(['->XEngine', 'xhost']) 531 | 532 | new_dict_mapping = {} 533 | for host, values in new_mapping.iteritems(): 534 | host_ = host[1:] 535 | new_dict_mapping[host_] = values 536 | # Update mappings 537 | _ = [listA.insert(_index, listA.pop(self.get_list_index(_sig, listA))) 538 | for _, listA in new_dict_mapping.iteritems() 539 | for _index, _sig in enumerate(fhost_sig_chain) 540 | ] 541 | 542 | return new_dict_mapping 543 | 544 | @property 545 | def get_original_mapped_sensors(self): 546 | mapping = [] 547 | for key, value in self.original_sensors.iteritems(): 548 | host = key.split(".")[0].lower() 549 | if host[1:].startswith("host"): 550 | if value[0] != "nominal": 551 | value.insert(0, key) 552 | new_dict = dict( 553 | izip_longest(*[iter([host, value])] * 2, fillvalue="") 554 | ) 555 | mapping.append(new_dict) 556 | 557 | new_mapping = combined_Dict_List(*mapping) 558 | return new_mapping 559 | 560 | @staticmethod 561 | def merged_sensors_dict(dict1, dict2): 562 | """ 563 | merge two dictionaries 564 | https://stackoverflow.com/questions/38987/how-to-merge-two-dictionaries-in-a-single-expression#26853961 565 | """ 566 | for key, value in dict2.iteritems(): 567 | dict1.setdefault(key, []).extend(value) 568 | return dict1 569 | # merged_sensors = f_sensors.copy() 570 | # merged_sensors.update(x_sensors) 571 | # return merged_sensors 572 | 573 | def create_dumps_dir(self): 574 | """ 575 | Create json dumps directory 576 | """ 577 | # Conflicted: To store in /tmp or not to store in , that is the question 578 | try: 579 | _dir, _name = os.path.split(os.path.dirname(os.path.realpath(__file__))) 580 | except Exception: 581 | _dir, _name = os.path.split(os.path.dirname(os.path.realpath(__name__))) 582 | path = _dir + "/json_dumps" 583 | if not os.path.exists(path): 584 | self.logger.info("Created %s for storing json dumps." % path) 585 | os.makedirs(path) 586 | 587 | def write_sorted_sensors_to_file(self): 588 | try: 589 | sensors = self.merged_sensors_dict( 590 | self.map_fhost_sensors, self.map_xhost_sensors 591 | ) 592 | except Exception: 593 | self.logger.error( 594 | "Failed to map the host sensors", 595 | exc_info=True 596 | ) 597 | raise 598 | else: 599 | self.create_dumps_dir() 600 | try: 601 | cur_path = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0] 602 | except Exception: 603 | cur_path = os.path.split(os.path.dirname(os.path.abspath(__name__)))[0] 604 | else: 605 | _filename = "{}/json_dumps/{}.{}.sensor_values.json".format( 606 | cur_path, self.hostname, self.array_name) 607 | _sensor_filename = "{}/json_dumps/{}.{}.ordered_sensor_values.json".format( 608 | cur_path, self.hostname, self.array_name) 609 | self.logger.info("Updating file: %s" % _filename) 610 | with open(_filename, "w") as outfile: 611 | json.dump(sensors, outfile, indent=4, sort_keys=True) 612 | with open(_sensor_filename, "w") as outfile: 613 | json.dump( 614 | self.get_original_mapped_sensors, 615 | outfile, 616 | indent=4, 617 | sort_keys=True, 618 | ) 619 | self.logger.info("Updated: %s" % _filename) 620 | 621 | 622 | if __name__ == "__main__": 623 | parser = argparse.ArgumentParser(description="Receive data from a CBF and play.") 624 | parser.add_argument( 625 | "--hostip", 626 | dest="katcp_host_ip", 627 | action="store", 628 | help="IP address of primary interface [eg: 10.103.254.6]", 629 | required=True, 630 | ) 631 | parser.add_argument( 632 | "--port", 633 | dest="katcp_host_port", 634 | action="store", 635 | default=7147, 636 | help="Primary Port to connect to. [Default: 7147]", 637 | ) 638 | parser.add_argument( 639 | "--poll-time", 640 | dest="poll", 641 | action="store", 642 | default=10, 643 | type=int, 644 | help="Poll the sensors every x seconds [Default: 10]", 645 | ) 646 | parser.add_argument( 647 | "--loglevel", 648 | dest="log_level", 649 | action="store", 650 | default="INFO", 651 | help="log level to use, default INFO, options INFO, DEBUG, ERROR", 652 | ) 653 | 654 | argcomplete.autocomplete(parser) 655 | args = vars(parser.parse_args()) 656 | pp = PrettyPrinter(indent=4) 657 | log_level = None 658 | log_format = "%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(pathname)s : %(lineno)d - %(message)s" 659 | if args.get("log_level", "INFO"): 660 | log_level = args.get("log_level", "INFO").upper() 661 | try: 662 | logging.basicConfig(level=getattr(logging, log_level), format=log_format) 663 | except AttributeError: 664 | raise RuntimeError("No such log level: %s" % log_level) 665 | else: 666 | if log_level == "DEBUG": 667 | coloredlogs.install(level=log_level, fmt=log_format) 668 | else: 669 | coloredlogs.install(level=log_level) 670 | 671 | katcp_ip = args.get("katcp_host_ip") 672 | katcp_port = args.get("katcp_host_port") 673 | 674 | sensor_poll = SensorPoll(katcp_ip, katcp_port) 675 | main_logger = LoggingClass() 676 | try: 677 | poll_time = args.get("poll") 678 | main_logger.logger.info("Begin sensor polling every %s seconds!!!" % poll_time) 679 | while True: 680 | sensor_poll.write_sorted_sensors_to_file() 681 | main_logger.logger.debug("Updating sensor on dashboard!!!") 682 | main_logger.logger.info("---------------------RELOADING SENSORS---------------------") 683 | time.sleep(poll_time) 684 | except Exception: 685 | main_logger.logger.error("Error occurred now breaking...") 686 | --------------------------------------------------------------------------------