__pycache__/
+dist/
controlpi_wsserver.egg-info/
venv/
--- /dev/null
+recursive-include doc/ *
+recursive-include controlpi_plugins/web/ *
"Example Server": {
"plugin": "WSServer",
"port": 8080,
- "web root": "web"
+ "web": {
+ "/": {"location": "web"},
+ "/Debug": {"module": "controlpi_plugins.wsserver",
+ "location": "Debug"}
+ }
},
"Example State": {
"plugin": "State"
--- /dev/null
+.clientcontainer {
+ padding-right: 5px;
+ padding-bottom: 5px;
+}
+
+.client {
+ display: inline-block;
+ vertical-align: top;
+ width: 600px;
+ margin-top: 5px;
+ margin-left: 5px;
+ border: 2px solid black;
+ padding: 5px;
+ background-color: #bcdaf8;
+}
+
+.client > header {
+ display: flex;
+ justify-content: space-between;
+}
+
+.client > header > h2 {
+ vertical-align: top;
+ font-size: 1.2rem;
+ font-weight: bold;
+}
+
+.client > header > h3 {
+ vertical-align: top;
+ font-size: 0.8rem;
+ font-weight: bold;
+}
+
+.interfacecontainer {
+ white-space: nowrap;
+}
+
+.interfacecontainer > h3 {
+ display: inline-block;
+ vertical-align: top;
+ width: 20px;
+ margin-top: 5px;
+ font-weight: bold;
+}
+
+.templatecontainer {
+ display: inline-block;
+ vertical-align: top;
+ width: 580px;
+ white-space: normal;
+}
+
+.lastcontainer {
+ text-align: center;
+}
+
+.lastcontainer * {
+ text-align: left;
+}
+
+.message {
+ display: inline-block;
+ vertical-align: top;
+ background-color: #79b5e7;
+ border: 1px solid black;
+}
+
+.message.green {
+ background-color: #85be71;
+}
+
+.message.red {
+ background-color: #ee96a8;
+}
+
+.templatecontainer > .message {
+ font-size: 0.8rem;
+ margin-top: 5px;
+ margin-left: 5px;
+}
+
+.lastcontainer > .message {
+ margin-top: 5px;
+}
+
+.message > span {
+ margin: 5px;
+ font-size: 1rem;
+}
+
+.message > h4 {
+ margin: 2px;
+ font-size: 0.8rem;
+}
+
+.message table {
+ border-spacing: 2px;
+}
+
+.message .array {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+}
+
+.message .array > * {
+ vertical-align: baseline;
+}
+
+.message .object {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid black;
+ padding-top: 2px;
+ padding-left: 2px;
+ margin-bottom: 2px;
+}
+
+.message .object > .property {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+}
+
+.message .object > .property > .key {
+ margin-right: 2px;
+ margin-bottom: 2px;
+}
+
+.message .object > .property > .value {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ margin-right: 2px;
+ margin-bottom: 2px;
+}
+
+.message input {
+ background-color: #35bfd3;
+}
+
+.message input[readonly] {
+ background-color: #79b5e7;
+}
+
+.message input:hover,
+.message input:focus {
+ background-color: #afa7ec;
+}
+
+.message input::selection {
+ background-color: #d69ae2;
+}
+
+.message input[type="submit"] {
+ border: 1px solid #0f4a53;
+ border-radius: 12px;
+ padding: 2px;
+}
+
+.message input[type="submit"]:hover,
+.message input[type="submit"]:focus {
+ border: 1px solid #44405e;
+}
+
+.message input[type="text"],
+.message input[type="number"] {
+ min-width: 5px;
+}
+
+input[type="number"]::-webkit-outer-spin-button,
+input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+}
+input[type="number"] {
+ -moz-appearance: textfield;
+}
+
+.message select {
+ background-color: #35bfd3;
+}
+
+.message select:hover,
+.message select:focus {
+ background-color: #afa7ec;
+}
--- /dev/null
+// Create section for client:
+function createForClient(client, plugin) {
+ const section = document.createElement('section')
+ section.setAttribute('id', client)
+ section.setAttribute('class', 'client')
+ const heading = document.createElement('header')
+ const headingH2 = document.createElement('h2')
+ headingH2.appendChild(document.createTextNode(client))
+ heading.appendChild(headingH2)
+ const headingH3 = document.createElement('h3')
+ headingH3.appendChild(document.createTextNode(plugin))
+ heading.appendChild(headingH3)
+ section.appendChild(heading)
+ const receiveOuter = document.createElement('div')
+ receiveOuter.setAttribute('class', 'interfacecontainer')
+ const receiveHeading = document.createElement('h3')
+ receiveHeading.appendChild(document.createTextNode('=>'))
+ receiveOuter.appendChild(receiveHeading)
+ const receiveInner = document.createElement('div')
+ receiveInner.setAttribute('id', client + ' Receives')
+ receiveInner.setAttribute('class', 'templatecontainer')
+ receiveOuter.appendChild(receiveInner)
+ section.appendChild(receiveOuter)
+ const sendOuter = document.createElement('div')
+ sendOuter.setAttribute('class', 'interfacecontainer')
+ const sendHeading = document.createElement('h3')
+ sendHeading.appendChild(document.createTextNode('<='))
+ sendOuter.appendChild(sendHeading)
+ const sendInner = document.createElement('div')
+ sendInner.setAttribute('id', client + ' Sends')
+ sendInner.setAttribute('class', 'templatecontainer')
+ sendOuter.appendChild(sendInner)
+ section.appendChild(sendOuter)
+ const last = document.createElement('div')
+ last.setAttribute('id', client + ' Last')
+ last.setAttribute('class', 'lastcontainer')
+ section.appendChild(last)
+ return section
+}
+
+// Create div and table for template:
+function createForTemplate(template) {
+ const div = document.createElement('div')
+ div.setAttribute('class', 'message')
+ if (Object.keys(template).length === 0) {
+ // Create span with '*' for empty templates:
+ const span = document.createElement('span')
+ const spanContent = document.createTextNode('*')
+ span.appendChild(spanContent)
+ div.appendChild(span)
+ } else {
+ const table = document.createElement('table')
+ for (const key in template) {
+ // Append table row for key-value pair:
+ const tr = document.createElement('tr')
+ const keyTd = document.createElement('td')
+ const keyTdContent = document.createTextNode(key + ':')
+ keyTd.appendChild(keyTdContent)
+ tr.appendChild(keyTd)
+ const valueTd = document.createElement('td')
+ schema = template[key]
+ value = ''
+ if ('const' in schema) {
+ value = JSON.stringify(schema['const'])
+ } else {
+ value = JSON.stringify(schema)
+ }
+ const valueTdContent = document.createTextNode(value)
+ valueTd.appendChild(valueTdContent)
+ tr.appendChild(valueTd)
+ table.appendChild(tr)
+ }
+ div.appendChild(table)
+ }
+ return div
+}
+
+// Resize input fields to current input:
+function resizeInput() {
+ const tmp = document.createElement('span')
+ // HACK: Should get this from CSS rather than hardcode it:
+ tmp.setAttribute('style', 'font-size: 0.8rem;')
+ var value = this.value
+ // HACK: 'number' fields for floats do not return trailing dot in
+ // value, so we add one unconditionally:
+ if (this.type == 'number' && this.step == 'any' && !value.includes('.')) {
+ value += '.'
+ }
+ const tmpContent = document.createTextNode(value)
+ tmp.appendChild(tmpContent)
+ document.body.appendChild(tmp);
+ const width = tmp.getBoundingClientRect().width
+ document.body.removeChild(tmp);
+ this.style.width = width + 'px'
+}
+
+// Create form input (or select) for key-value pairs in receive templates:
+function inputsForKeyValue(key, schema) {
+ result = []
+ literal = false
+ if ('const' in schema) {
+ literal = true
+ }
+ if (schema['type'] == 'boolean' ||
+ (literal && typeof(schema['const']) == 'boolean')) {
+ // Create select with true and false options for Boolean:
+ const select = document.createElement('select')
+ select.setAttribute('name', key)
+ const optionTrue = document.createElement('option')
+ optionTrue.setAttribute('value', 'true')
+ const optionTrueContent = document.createTextNode('true')
+ optionTrue.appendChild(optionTrueContent)
+ select.appendChild(optionTrue)
+ const optionFalse = document.createElement('option')
+ optionFalse.setAttribute('value', false)
+ const optionFalseContent = document.createTextNode('false')
+ optionFalse.appendChild(optionFalseContent)
+ if (literal) {
+ // Select set value and disable other value
+ // for literal Boolean:
+ if (schema['const']) {
+ optionTrue.setAttribute('selected', '')
+ optioniFalse.setAttribute('disabled', '')
+ } else {
+ optionFalse.setAttribute('selected', '')
+ optionTrue.setAttribute('disabled', '')
+ }
+ }
+ select.appendChild(optionFalse)
+ result.push(select)
+ } else {
+ // Create input for everything except Booleans:
+ if (schema['type'] == 'string' ||
+ (literal && typeof(schema['const']) == 'string' &&
+ key != 'command')) {
+ // Quote strings:
+ const openquote = document.createTextNode('"')
+ result.push(openquote)
+ }
+ const input = document.createElement('input')
+ // Set type of input:
+ if (key == 'command') {
+ input.setAttribute('type', 'hidden')
+ } else if (schema['type'] == 'integer' ||
+ schema['type'] == 'number' ||
+ typeof(schema['const']) == 'number') {
+ input.setAttribute('type', 'number')
+ if (schema['type'] == 'integer') {
+ input.setAttribute('step', '1')
+ } else if (schema['type'] == 'number') {
+ input.setAttribute('step', 'any')
+ }
+ } else if (schema['type'] == 'string' ||
+ typeof(schema['const']) == 'string') {
+ input.setAttribute('type', 'text')
+ }
+ // Set key as name of input:
+ input.setAttribute('name', key)
+ // Set value of input, readonly or required:
+ if (key == 'command') {
+ input.setAttribute('value', schema['const'])
+ } else if (literal) {
+ input.setAttribute('value', schema['const'])
+ input.setAttribute('readonly', '')
+ } else {
+ input.setAttribute('value', '')
+ input.setAttribute('required', '')
+ }
+ if (key != 'command') {
+ // Resize input field at every change and at beginning:
+ input.addEventListener('input', resizeInput)
+ resizeInput.call(input)
+ }
+ result.push(input)
+ if (schema['type'] == 'string' ||
+ (literal && typeof(schema['const']) == 'string' &&
+ key != 'command')) {
+ // Quote strings:
+ const closequote = document.createTextNode('"')
+ result.push(closequote)
+ }
+ if (key == 'command') {
+ // Add submit button for 'command' key:
+ const submit = document.createElement('input')
+ submit.setAttribute('type', 'submit')
+ submit.setAttribute('value', schema['const'])
+ result.push(submit)
+ }
+ }
+ return result
+}
+
+// Create div, form and table for command template:
+function createForCommand(template) {
+ const div = document.createElement('div')
+ div.setAttribute('class', 'message')
+ const form = document.createElement('form')
+ form.addEventListener("submit", function(e) {
+ // Convert names and values of all form elements to key-value pairs
+ // and send as JSON over websocket:
+ e.preventDefault();
+ var data = {};
+ for (const element of form.elements) {
+ if (element.type == 'text' || element.type == 'hidden') {
+ data[element.name] = element.value
+ } else if (element.type == 'number') {
+ data[element.name] = element.valueAsNumber
+ } else if (element.type == 'select-one') {
+ if (element.value == 'true') {
+ data[element.name] = true
+ } else if (element.value == 'false') {
+ data[element.name] = false
+ } else {
+ data[element.name] = element.value
+ }
+ }
+ }
+ websocket.send(JSON.stringify(data))
+ })
+ const table = document.createElement('table')
+ for (const key in template) {
+ // Append table row for key-value pair:
+ const tr = document.createElement('tr')
+ const keyTd = document.createElement('td')
+ const keyTdContent = document.createTextNode(key + ':')
+ keyTd.appendChild(keyTdContent)
+ tr.appendChild(keyTd)
+ const valueTd = document.createElement('td')
+ for (const element of inputsForKeyValue(key, template[key])) {
+ valueTd.appendChild(element)
+ }
+ tr.appendChild(valueTd)
+ table.appendChild(tr)
+ }
+ form.appendChild(table)
+ div.appendChild(form)
+ return div
+}
+
+// Remove (if unregistered) or create (if not existent) elements for
+// clients and update interface information:
+function processBusMessage(message) {
+ if (message['event'] == 'unregistered') {
+ // On deregistration delete client element if it exists:
+ const clientElement = document.getElementById(message['client'])
+ if (clientElement != null) {
+ clientElement.remove()
+ }
+ } else {
+ // On registration or 'get clients' answer:
+ const clientElement = document.getElementById(message['client'])
+ if (clientElement == null) {
+ // Create element for client if not existent:
+ const main = document.getElementById('ControlPi Debug')
+ main.appendChild(createForClient(message['client'],
+ message['plugin']))
+ }
+ // Crate message elements for receives interface:
+ const receiveContainer = document.getElementById(message['client'] + ' Receives')
+ receiveContainer.innerHTML = ''
+ for (const template of message['receives']) {
+ if (template['command'] != null) {
+ receiveContainer.appendChild(createForCommand(template))
+ } else {
+ receiveContainer.appendChild(createForTemplate(template))
+ }
+ }
+ // Create message elements for sends interface:
+ const sendContainer = document.getElementById(message['client'] + ' Sends')
+ sendContainer.innerHTML = ''
+ for (const template of message['sends']) {
+ sendContainer.appendChild(createForTemplate(template))
+ }
+ }
+}
+
+// Recursively create div structure for arrays, objects and primitive
+// values:
+function createForMessageValue(value) {
+ var result = ''
+ if (typeof(value) == 'object') {
+ if (Array.isArray(value)) {
+ result = document.createElement('div')
+ result.setAttribute('class', 'array')
+ var counter = value.length
+ for (const child of value) {
+ result.appendChild(createForMessageValue(child))
+ if (--counter) {
+ result.appendChild(document.createTextNode(', '))
+ }
+ }
+ } else {
+ result = document.createElement('div')
+ result.setAttribute('class', 'object')
+ for (const key in value) {
+ const property = document.createElement('div')
+ property.setAttribute('class', 'property')
+ const keyDiv = document.createElement('div')
+ keyDiv.setAttribute('class', 'key')
+ keyDiv.appendChild(document.createTextNode(key + ':'))
+ property.appendChild(keyDiv)
+ const valueDiv = document.createElement('div')
+ valueDiv.setAttribute('class', 'value')
+ valueDiv.appendChild(createForMessageValue(value[key]))
+ property.appendChild(valueDiv)
+ result.appendChild(property)
+ }
+ }
+ } else {
+ result = document.createTextNode(JSON.stringify(value))
+ }
+ return result
+}
+
+// Create div and table for message:
+function createForMessage(message) {
+ const div = document.createElement('div')
+ div.setAttribute('class', 'message')
+ // Current (receive) time as heading:
+ const time = new Date().toLocaleTimeString()
+ const h4 = document.createElement('h4')
+ const h4Content = document.createTextNode(time)
+ h4.appendChild(h4Content)
+ div.appendChild(h4)
+ const table = document.createElement('table')
+ for (const key in message) {
+ if (key == 'sender') {
+ // Ignore 'sender' for last received messages
+ // (information redundantly present in client heading):
+ continue
+ } else if (key == 'state') {
+ // Set background according to state:
+ if (message[key] === true) {
+ div.classList.add('green')
+ } else if (message[key] === false) {
+ div.classList.add('red')
+ }
+ }
+ // Append table row for key-value pair:
+ const tr = document.createElement('tr')
+ const keyTd = document.createElement('td')
+ const keyTdContent = document.createTextNode(key + ':')
+ keyTd.appendChild(keyTdContent)
+ tr.appendChild(keyTd)
+ const valueTd = document.createElement('td')
+ valueTd.appendChild(createForMessageValue(message[key]))
+ tr.appendChild(valueTd)
+ table.appendChild(tr)
+ }
+ div.appendChild(table)
+ return div
+}
+
+// Create element for client (if not existent)
+// and update last received message:
+function processClientMessage(message) {
+ const clientElement = document.getElementById(message['sender'])
+ if (clientElement == null) {
+ // Create section for client if not existent:
+ const main = document.getElementById('ControlPi Debug')
+ main.appendChild(createForClient(message.sender, ''))
+ }
+ // Update last received message:
+ const lastContainer = document.getElementById(message['sender'] + ' Last')
+ lastContainer.innerHTML = ''
+ lastContainer.appendChild(createForMessage(message))
+}
+
+// Open Websocket back to ControlPi we were loaded from:
+const websocket = new WebSocket("ws://" + window.location.host)
+
+// When Websocket is ready request all clients from bus:
+websocket.addEventListener('open', function (event) {
+ websocket.send(JSON.stringify({target: '', command: 'get clients'}))
+})
+
+// When receiving message from ControlPi through Websocket:
+websocket.addEventListener('message', function (event) {
+ const message = JSON.parse(event.data)
+ if (message['sender'] == '') {
+ processBusMessage(message)
+ } else {
+ processClientMessage(message)
+ }
+})
--- /dev/null
+@import url('https://fonts.googleapis.com/css?family=Roboto');
+@import url('https://fonts.googleapis.com/css?family=Michroma');
+
+* {
+ vertical-align: baseline;
+ margin: 0;
+ outline: 0;
+ border: 0 none;
+ padding: 0;
+ font-family: 'Roboto', sans-serif;
+ font-weight: inherit;
+ font-style: inherit;
+ font-size: inherit;
+}
+
+html {
+ font-size: 16px;
+}
+
+body {
+ background-color: white;
+ color: black;
+}
+
+body > header {
+ padding: 10px;
+ background-color: #2c343b;
+}
+
+body > header * {
+ vertical-align: middle;
+}
+
+body > header svg {
+ height: 40px;
+}
+
+body > header span {
+ font-family: "Michroma", sans-serif;
+ font-weight: bold;
+ font-size: 20px;
+ text-transform: uppercase;
+}
+
+body > header span.Graph {
+ margin-left: 10px;
+ color: white;
+}
+
+body > header span.IT {
+ margin-right: 10px;
+ color: #83a1b4;
+}
+
+h1, h2, h3 {
+ margin-top: 5px;
+ margin-left: 5px;
+ font-weight: bold;
+}
+
+h1 {
+ font-size: 1.4rem;
+}
+
+h2 {
+ font-size: 1.2rem;
+}
+
+h3 {
+ font-size: 1.2rem;
+}
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>ControlPi Debug</title>
+ <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=">
+ <link rel="stylesheet" type="text/css" href="index.css">
+ <link rel="stylesheet" type="text/css" href="controlpi-debug.css">
+ <script src="controlpi-debug.js" defer></script>
+ </head>
+ <body>
+ <header>
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 334 299">
+ <g stroke="#83a1b4" stroke-width="8" fill="none">
+ <circle r="14.5" transform="translate(18.53 235.35)"/>
+ <circle r="14.5" transform="translate(40.39 75.08)"/>
+ <circle r="14.5" transform="translate(198.04 20.48)"/>
+ <circle r="14.5" transform="translate(313.77 145.08)"/>
+ <circle r="14.5" transform="translate(145.7 131.46)"/>
+ <circle r="14.5" transform="translate(209.99 279.89)"/>
+ <path stroke-width="3.7" d="M38.8 241l149 34.8m34.7-12.3l77-100m-261 67l253.4-78M34 222.5L128 146M20.4 214.7l16-117m23.3-29l116.8-41.8M154 112.6L188 39m-21.5 95L291 144M58.6 86.5l67 35.7m86.2-86.6L298 130"/>
+ </g>
+ </svg>
+ <span class="Graph">Graph-</span><span class="IT">IT</span>
+ </header>
+ <h1>ControlPi Debug</h1>
+ <main id="ControlPi Debug" class="clientcontainer">
+ </main>
+ </body>
+</html>
…
TODO: documentation, doctests
-TODO: Mount multiple web apps from packages and file paths
TODO: Let Debug web app collapse/expand nested structures
TODO: Make Debug web app work with nested structures in commands
+TODO: Let clients filter messages received over websocket
"""
import os
+import sys
import json
from websockets import (WebSocketServerProtocol, ConnectionClosedOK,
ConnectionClosedError, serve)
from websockets.http import Headers
from http import HTTPStatus
-from typing import Optional, Tuple
from controlpi import BasePlugin, MessageBus, Message, MessageTemplate
+from typing import Optional, Tuple
+
class Connection:
"""Connection to websocket.
the contents in given web root.
"""
- CONF_SCHEMA = {'properties': {'host': {'type': 'string'},
- 'port': {'type': 'integer'},
- 'web root': {'type': 'string'}}}
+ CONF_SCHEMA = {'properties':
+ {'host': {'type': 'string'},
+ 'port': {'type': 'integer'},
+ 'web': {'type': 'object',
+ 'patternProperties':
+ {'^/([A-Z][A-Za-z]*)?$':
+ {'type': 'object',
+ 'properties': {'module': {'type': 'string'},
+ 'location': {'type': 'string'}},
+ 'required': ['location']}},
+ 'additionalProperties': False}}}
"""Schema for WServer plugin configuration.
Optional configuration keys:
- 'host': network interfaces to listen on (default: None, meaning all
interfaces)
- 'port': port to connect to (default: 80)
- - 'web root': root of files to serve (default: 'web')
+ - 'web': mapping of web paths to locations on disk (either as absolute
+ path, relative path from the working directory or path from the
+ path containing a given module)
"""
async def _handler(self, websocket: WebSocketServerProtocol,
request_headers: Headers) -> Response:
if 'Upgrade' in request_headers:
return None
- if path == '/':
- path = '/index.html'
response_headers = Headers()
response_headers['Server'] = 'controlpi-wsserver websocket server'
response_headers['Connection'] = 'close'
- file_path = os.path.realpath(os.path.join(self._web_root, path[1:]))
- if os.path.commonpath((self._web_root, file_path)) != self._web_root \
- or not os.path.exists(file_path) \
- or not os.path.isfile(file_path):
+ if path not in self._web_files:
return (HTTPStatus.NOT_FOUND, response_headers,
f"File '{path}' not found!".encode())
- mime_type = 'application/octet-stream'
- extension = file_path.split('.')[-1]
- if extension == 'html':
- mime_type = 'text/html'
- elif extension == 'js':
- mime_type = 'text/javascript'
- elif extension == 'css':
- mime_type = 'text/css'
- response_headers['Content-Type'] = mime_type
- body = open(file_path, 'rb').read()
+ file_info = self._web_files[path]
+ response_headers['Content-Type'] = file_info['type']
+ body = open(file_info['location'], 'rb').read()
response_headers['Content-Length'] = str(len(body))
return HTTPStatus.OK, response_headers, body
def process_conf(self) -> None:
"""Get host, port and path settings from configuration."""
+ self._host = None
+ if 'host' in self.conf:
+ self._host = self.conf['host']
+ else:
+ print(f"'host' not configured for WSServer '{self.name}'."
+ " Serving on all interfaces.")
self._port = 80
if 'port' in self.conf:
self._port = self.conf['port']
else:
print(f"'port' not configured for WSServer '{self.name}'."
- " Using 80.")
- web_root = 'web'
- if 'web root' in self.conf:
- web_root = self.conf['web root']
- else:
- print(f"'web root' not configured for WSServer '{self.name}'."
- " Using 'web'.")
- self._web_root = os.path.realpath(os.path.join(os.getcwd(),
- web_root))
+ " Using port 80.")
+ self._web_files = {}
+ if 'web' in self.conf:
+ for path in self.conf['web']:
+ path_conf = self.conf['web'][path]
+ location = path_conf['location']
+ if 'module' in path_conf:
+ # Determine location relative to module directory:
+ module_file = sys.modules[path_conf['module']].__file__
+ module_dir = os.path.dirname(module_file)
+ location = os.path.join(module_dir, 'web', location)
+ else:
+ # Determine location relative to current working directory:
+ location = os.path.join(os.getcwd(), location)
+ location = os.path.realpath(location)
+ # Walk all files in location recursively:
+ for (dir_location, _, filenames) in os.walk(location):
+ dir_path = os.path.join(path, dir_location[len(location):])
+ for filename in filenames:
+ file_path = os.path.join(dir_path, filename)
+ file_location = os.path.join(dir_location, filename)
+ file_info = {'location': file_location}
+ # Determine MIME type:
+ file_info['type'] = 'application/octet-stream'
+ extension = filename.split('.')[-1]
+ if extension == 'html':
+ file_info['type'] = 'text/html'
+ elif extension == 'js':
+ file_info['type'] = 'text/javascript'
+ elif extension == 'css':
+ file_info['type'] = 'text/css'
+ # Register in instance variable:
+ self._web_files[file_path] = file_info
+ # Serve index.html also as directory:
+ if filename == 'index.html':
+ self._web_files[dir_path] = file_info
+ self._web_files[dir_path.rstrip('/')] = file_info
async def run(self) -> None:
"""Set up websocket server."""
- await serve(self._handler, port=self._port,
+ await serve(self._handler, host=self._host, port=self._port,
process_request=self._process_request)
print(f"WSServer '{self.name}' serving on port {self._port}.")
long_description_content_type="text/markdown",
url="http://docs.graph-it.com/graphit/controlpi-wsserver",
packages=["controlpi_plugins"],
+ include_package_data=True,
install_requires=[
"websockets",
"controlpi @ git+git://git.graph-it.com/graphit/controlpi.git",
+++ /dev/null
-.clientcontainer {
- padding-right: 5px;
- padding-bottom: 5px;
-}
-
-.client {
- display: inline-block;
- vertical-align: top;
- width: 600px;
- margin-top: 5px;
- margin-left: 5px;
- border: 2px solid black;
- padding: 5px;
- background-color: #bcdaf8;
-}
-
-.client > header {
- display: flex;
- justify-content: space-between;
-}
-
-.client > header > h2 {
- vertical-align: top;
- font-size: 1.2rem;
- font-weight: bold;
-}
-
-.client > header > h3 {
- vertical-align: top;
- font-size: 0.8rem;
- font-weight: bold;
-}
-
-.interfacecontainer {
- white-space: nowrap;
-}
-
-.interfacecontainer > h3 {
- display: inline-block;
- vertical-align: top;
- width: 20px;
- margin-top: 5px;
- font-weight: bold;
-}
-
-.templatecontainer {
- display: inline-block;
- vertical-align: top;
- width: 580px;
- white-space: normal;
-}
-
-.lastcontainer {
- text-align: center;
-}
-
-.lastcontainer * {
- text-align: left;
-}
-
-.message {
- display: inline-block;
- vertical-align: top;
- background-color: #79b5e7;
- border: 1px solid black;
-}
-
-.message.green {
- background-color: #85be71;
-}
-
-.message.red {
- background-color: #ee96a8;
-}
-
-.templatecontainer > .message {
- font-size: 0.8rem;
- margin-top: 5px;
- margin-left: 5px;
-}
-
-.lastcontainer > .message {
- margin-top: 5px;
-}
-
-.message > span {
- margin: 5px;
- font-size: 1rem;
-}
-
-.message > h4 {
- margin: 2px;
- font-size: 0.8rem;
-}
-
-.message table {
- border-spacing: 2px;
-}
-
-.message .array {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
-}
-
-.message .array > * {
- vertical-align: baseline;
-}
-
-.message .object {
- display: flex;
- flex-direction: column;
- border: 1px solid black;
- padding-top: 2px;
- padding-left: 2px;
- margin-bottom: 2px;
-}
-
-.message .object > .property {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
-}
-
-.message .object > .property > .key {
- margin-right: 2px;
- margin-bottom: 2px;
-}
-
-.message .object > .property > .value {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- margin-right: 2px;
- margin-bottom: 2px;
-}
-
-.message input {
- background-color: #35bfd3;
-}
-
-.message input[readonly] {
- background-color: #79b5e7;
-}
-
-.message input:hover,
-.message input:focus {
- background-color: #afa7ec;
-}
-
-.message input::selection {
- background-color: #d69ae2;
-}
-
-.message input[type="submit"] {
- border: 1px solid #0f4a53;
- border-radius: 12px;
- padding: 2px;
-}
-
-.message input[type="submit"]:hover,
-.message input[type="submit"]:focus {
- border: 1px solid #44405e;
-}
-
-.message input[type="text"],
-.message input[type="number"] {
- min-width: 5px;
-}
-
-input[type="number"]::-webkit-outer-spin-button,
-input[type="number"]::-webkit-inner-spin-button {
- -webkit-appearance: none;
-}
-input[type="number"] {
- -moz-appearance: textfield;
-}
-
-.message select {
- background-color: #35bfd3;
-}
-
-.message select:hover,
-.message select:focus {
- background-color: #afa7ec;
-}
+++ /dev/null
-// Create section for client:
-function createForClient(client, plugin) {
- const section = document.createElement('section')
- section.setAttribute('id', client)
- section.setAttribute('class', 'client')
- const heading = document.createElement('header')
- const headingH2 = document.createElement('h2')
- headingH2.appendChild(document.createTextNode(client))
- heading.appendChild(headingH2)
- const headingH3 = document.createElement('h3')
- headingH3.appendChild(document.createTextNode(plugin))
- heading.appendChild(headingH3)
- section.appendChild(heading)
- const receiveOuter = document.createElement('div')
- receiveOuter.setAttribute('class', 'interfacecontainer')
- const receiveHeading = document.createElement('h3')
- receiveHeading.appendChild(document.createTextNode('=>'))
- receiveOuter.appendChild(receiveHeading)
- const receiveInner = document.createElement('div')
- receiveInner.setAttribute('id', client + ' Receives')
- receiveInner.setAttribute('class', 'templatecontainer')
- receiveOuter.appendChild(receiveInner)
- section.appendChild(receiveOuter)
- const sendOuter = document.createElement('div')
- sendOuter.setAttribute('class', 'interfacecontainer')
- const sendHeading = document.createElement('h3')
- sendHeading.appendChild(document.createTextNode('<='))
- sendOuter.appendChild(sendHeading)
- const sendInner = document.createElement('div')
- sendInner.setAttribute('id', client + ' Sends')
- sendInner.setAttribute('class', 'templatecontainer')
- sendOuter.appendChild(sendInner)
- section.appendChild(sendOuter)
- const last = document.createElement('div')
- last.setAttribute('id', client + ' Last')
- last.setAttribute('class', 'lastcontainer')
- section.appendChild(last)
- return section
-}
-
-// Create div and table for template:
-function createForTemplate(template) {
- const div = document.createElement('div')
- div.setAttribute('class', 'message')
- if (Object.keys(template).length === 0) {
- // Create span with '*' for empty templates:
- const span = document.createElement('span')
- const spanContent = document.createTextNode('*')
- span.appendChild(spanContent)
- div.appendChild(span)
- } else {
- const table = document.createElement('table')
- for (const key in template) {
- // Append table row for key-value pair:
- const tr = document.createElement('tr')
- const keyTd = document.createElement('td')
- const keyTdContent = document.createTextNode(key + ':')
- keyTd.appendChild(keyTdContent)
- tr.appendChild(keyTd)
- const valueTd = document.createElement('td')
- schema = template[key]
- value = ''
- if ('const' in schema) {
- value = JSON.stringify(schema['const'])
- } else {
- value = JSON.stringify(schema)
- }
- const valueTdContent = document.createTextNode(value)
- valueTd.appendChild(valueTdContent)
- tr.appendChild(valueTd)
- table.appendChild(tr)
- }
- div.appendChild(table)
- }
- return div
-}
-
-// Resize input fields to current input:
-function resizeInput() {
- const tmp = document.createElement('span')
- // HACK: Should get this from CSS rather than hardcode it:
- tmp.setAttribute('style', 'font-size: 0.8rem;')
- var value = this.value
- // HACK: 'number' fields for floats do not return trailing dot in
- // value, so we add one unconditionally:
- if (this.type == 'number' && this.step == 'any' && !value.includes('.')) {
- value += '.'
- }
- const tmpContent = document.createTextNode(value)
- tmp.appendChild(tmpContent)
- document.body.appendChild(tmp);
- const width = tmp.getBoundingClientRect().width
- document.body.removeChild(tmp);
- this.style.width = width + 'px'
-}
-
-// Create form input (or select) for key-value pairs in receive templates:
-function inputsForKeyValue(key, schema) {
- result = []
- literal = false
- if ('const' in schema) {
- literal = true
- }
- if (schema['type'] == 'boolean' ||
- (literal && typeof(schema['const']) == 'boolean')) {
- // Create select with true and false options for Boolean:
- const select = document.createElement('select')
- select.setAttribute('name', key)
- const optionTrue = document.createElement('option')
- optionTrue.setAttribute('value', 'true')
- const optionTrueContent = document.createTextNode('true')
- optionTrue.appendChild(optionTrueContent)
- select.appendChild(optionTrue)
- const optionFalse = document.createElement('option')
- optionFalse.setAttribute('value', false)
- const optionFalseContent = document.createTextNode('false')
- optionFalse.appendChild(optionFalseContent)
- if (literal) {
- // Select set value and disable other value
- // for literal Boolean:
- if (schema['const']) {
- optionTrue.setAttribute('selected', '')
- optioniFalse.setAttribute('disabled', '')
- } else {
- optionFalse.setAttribute('selected', '')
- optionTrue.setAttribute('disabled', '')
- }
- }
- select.appendChild(optionFalse)
- result.push(select)
- } else {
- // Create input for everything except Booleans:
- if (schema['type'] == 'string' ||
- (literal && typeof(schema['const']) == 'string' &&
- key != 'command')) {
- // Quote strings:
- const openquote = document.createTextNode('"')
- result.push(openquote)
- }
- const input = document.createElement('input')
- // Set type of input:
- if (key == 'command') {
- input.setAttribute('type', 'hidden')
- } else if (schema['type'] == 'integer' ||
- schema['type'] == 'number' ||
- typeof(schema['const']) == 'number') {
- input.setAttribute('type', 'number')
- if (schema['type'] == 'integer') {
- input.setAttribute('step', '1')
- } else if (schema['type'] == 'number') {
- input.setAttribute('step', 'any')
- }
- } else if (schema['type'] == 'string' ||
- typeof(schema['const']) == 'string') {
- input.setAttribute('type', 'text')
- }
- // Set key as name of input:
- input.setAttribute('name', key)
- // Set value of input, readonly or required:
- if (key == 'command') {
- input.setAttribute('value', schema['const'])
- } else if (literal) {
- input.setAttribute('value', schema['const'])
- input.setAttribute('readonly', '')
- } else {
- input.setAttribute('value', '')
- input.setAttribute('required', '')
- }
- if (key != 'command') {
- // Resize input field at every change and at beginning:
- input.addEventListener('input', resizeInput)
- resizeInput.call(input)
- }
- result.push(input)
- if (schema['type'] == 'string' ||
- (literal && typeof(schema['const']) == 'string' &&
- key != 'command')) {
- // Quote strings:
- const closequote = document.createTextNode('"')
- result.push(closequote)
- }
- if (key == 'command') {
- // Add submit button for 'command' key:
- const submit = document.createElement('input')
- submit.setAttribute('type', 'submit')
- submit.setAttribute('value', schema['const'])
- result.push(submit)
- }
- }
- return result
-}
-
-// Create div, form and table for command template:
-function createForCommand(template) {
- const div = document.createElement('div')
- div.setAttribute('class', 'message')
- const form = document.createElement('form')
- form.addEventListener("submit", function(e) {
- // Convert names and values of all form elements to key-value pairs
- // and send as JSON over websocket:
- e.preventDefault();
- var data = {};
- for (const element of form.elements) {
- if (element.type == 'text' || element.type == 'hidden') {
- data[element.name] = element.value
- } else if (element.type == 'number') {
- data[element.name] = element.valueAsNumber
- } else if (element.type == 'select-one') {
- if (element.value == 'true') {
- data[element.name] = true
- } else if (element.value == 'false') {
- data[element.name] = false
- } else {
- data[element.name] = element.value
- }
- }
- }
- websocket.send(JSON.stringify(data))
- })
- const table = document.createElement('table')
- for (const key in template) {
- // Append table row for key-value pair:
- const tr = document.createElement('tr')
- const keyTd = document.createElement('td')
- const keyTdContent = document.createTextNode(key + ':')
- keyTd.appendChild(keyTdContent)
- tr.appendChild(keyTd)
- const valueTd = document.createElement('td')
- for (const element of inputsForKeyValue(key, template[key])) {
- valueTd.appendChild(element)
- }
- tr.appendChild(valueTd)
- table.appendChild(tr)
- }
- form.appendChild(table)
- div.appendChild(form)
- return div
-}
-
-// Remove (if unregistered) or create (if not existent) elements for
-// clients and update interface information:
-function processBusMessage(message) {
- if (message['event'] == 'unregistered') {
- // On deregistration delete client element if it exists:
- const clientElement = document.getElementById(message['client'])
- if (clientElement != null) {
- clientElement.remove()
- }
- } else {
- // On registration or 'get clients' answer:
- const clientElement = document.getElementById(message['client'])
- if (clientElement == null) {
- // Create element for client if not existent:
- const main = document.getElementById('ControlPi Debug')
- main.appendChild(createForClient(message['client'],
- message['plugin']))
- }
- // Crate message elements for receives interface:
- const receiveContainer = document.getElementById(message['client'] + ' Receives')
- receiveContainer.innerHTML = ''
- for (const template of message['receives']) {
- if (template['command'] != null) {
- receiveContainer.appendChild(createForCommand(template))
- } else {
- receiveContainer.appendChild(createForTemplate(template))
- }
- }
- // Create message elements for sends interface:
- const sendContainer = document.getElementById(message['client'] + ' Sends')
- sendContainer.innerHTML = ''
- for (const template of message['sends']) {
- sendContainer.appendChild(createForTemplate(template))
- }
- }
-}
-
-// Recursively create div structure for arrays, objects and primitive
-// values:
-function createForMessageValue(value) {
- var result = ''
- if (typeof(value) == 'object') {
- if (Array.isArray(value)) {
- result = document.createElement('div')
- result.setAttribute('class', 'array')
- var counter = value.length
- for (const child of value) {
- result.appendChild(createForMessageValue(child))
- if (--counter) {
- result.appendChild(document.createTextNode(', '))
- }
- }
- } else {
- result = document.createElement('div')
- result.setAttribute('class', 'object')
- for (const key in value) {
- const property = document.createElement('div')
- property.setAttribute('class', 'property')
- const keyDiv = document.createElement('div')
- keyDiv.setAttribute('class', 'key')
- keyDiv.appendChild(document.createTextNode(key + ':'))
- property.appendChild(keyDiv)
- const valueDiv = document.createElement('div')
- valueDiv.setAttribute('class', 'value')
- valueDiv.appendChild(createForMessageValue(value[key]))
- property.appendChild(valueDiv)
- result.appendChild(property)
- }
- }
- } else {
- result = document.createTextNode(JSON.stringify(value))
- }
- return result
-}
-
-// Create div and table for message:
-function createForMessage(message) {
- const div = document.createElement('div')
- div.setAttribute('class', 'message')
- // Current (receive) time as heading:
- const time = new Date().toLocaleTimeString()
- const h4 = document.createElement('h4')
- const h4Content = document.createTextNode(time)
- h4.appendChild(h4Content)
- div.appendChild(h4)
- const table = document.createElement('table')
- for (const key in message) {
- if (key == 'sender') {
- // Ignore 'sender' for last received messages
- // (information redundantly present in client heading):
- continue
- } else if (key == 'state') {
- // Set background according to state:
- if (message[key] === true) {
- div.classList.add('green')
- } else if (message[key] === false) {
- div.classList.add('red')
- }
- }
- // Append table row for key-value pair:
- const tr = document.createElement('tr')
- const keyTd = document.createElement('td')
- const keyTdContent = document.createTextNode(key + ':')
- keyTd.appendChild(keyTdContent)
- tr.appendChild(keyTd)
- const valueTd = document.createElement('td')
- valueTd.appendChild(createForMessageValue(message[key]))
- tr.appendChild(valueTd)
- table.appendChild(tr)
- }
- div.appendChild(table)
- return div
-}
-
-// Create element for client (if not existent)
-// and update last received message:
-function processClientMessage(message) {
- const clientElement = document.getElementById(message['sender'])
- if (clientElement == null) {
- // Create section for client if not existent:
- const main = document.getElementById('ControlPi Debug')
- main.appendChild(createForClient(message.sender, ''))
- }
- // Update last received message:
- const lastContainer = document.getElementById(message['sender'] + ' Last')
- lastContainer.innerHTML = ''
- lastContainer.appendChild(createForMessage(message))
-}
-
-// Open Websocket back to ControlPi we were loaded from:
-const websocket = new WebSocket("ws://" + window.location.host)
-
-// When Websocket is ready request all clients from bus:
-websocket.addEventListener('open', function (event) {
- websocket.send(JSON.stringify({target: '', command: 'get clients'}))
-})
-
-// When receiving message from ControlPi through Websocket:
-websocket.addEventListener('message', function (event) {
- const message = JSON.parse(event.data)
- if (message['sender'] == '') {
- processBusMessage(message)
- } else {
- processClientMessage(message)
- }
-})
color: #83a1b4;
}
-h1 {
+h1, h2, h3 {
margin-top: 5px;
margin-left: 5px;
- margin-right: 5px;
- font-size: 1.4rem;
font-weight: bold;
}
+
+h1 {
+ font-size: 1.4rem;
+}
+
+h2 {
+ font-size: 1.2rem;
+}
+
+h3 {
+ font-size: 1.2rem;
+}
<title>ControlPi Debug</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=">
<link rel="stylesheet" type="text/css" href="index.css">
- <link rel="stylesheet" type="text/css" href="controlpi-debug.css">
- <script src="controlpi-debug.js" defer></script>
</head>
<body>
<header>
</svg>
<span class="Graph">Graph-</span><span class="IT">IT</span>
</header>
- <h1>ControlPi Debug</h1>
- <main id="ControlPi Debug" class="clientcontainer">
- </main>
+ <h1>ControlPi</h1>
+ <h2><a href="Debug/">Debug</a></h2>
</body>
</html>