Moved Python stuff to dedicated repo, preparing to clean up packagenaming
This commit is contained in:
parent
ea0a62598a
commit
208d7d05d7
|
@ -1,6 +0,0 @@
|
||||||
*/__pycache__/
|
|
||||||
bin/
|
|
||||||
include/
|
|
||||||
lib/
|
|
||||||
lib64
|
|
||||||
share/
|
|
|
@ -1 +0,0 @@
|
||||||
../index.html
|
|
|
@ -1,16 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="/static/tabledata.css">
|
|
||||||
<script src="/static/tabledata.js"></script>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Enervent Pingvin Kotilämpö</title>
|
|
||||||
</head>
|
|
||||||
<body onload="getData()">
|
|
||||||
<table id="data">
|
|
||||||
<caption>Coil values at <span id="time"></span></caption>
|
|
||||||
<thead><th>Address</th><th>Value</th><th>Symbol</th><th>Description</th></thead>
|
|
||||||
<tbody id="coildata"></tbody>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1 +0,0 @@
|
||||||
../index.html
|
|
|
@ -1,17 +0,0 @@
|
||||||
.addr {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.val {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#data {
|
|
||||||
padding: 2pt;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
thead {
|
|
||||||
border-bottom: 1px solid;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
padding: 2pt;
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
function zeroPad(number) {
|
|
||||||
return ("0" + number).slice(-2)
|
|
||||||
}
|
|
||||||
function getData() {
|
|
||||||
now = new Date()
|
|
||||||
Y = now.getFullYear()
|
|
||||||
m = now.getMonth()
|
|
||||||
d = now.getDate()
|
|
||||||
H = zeroPad(now.getHours())
|
|
||||||
M = zeroPad(now.getMinutes())
|
|
||||||
S = zeroPad(now.getSeconds())
|
|
||||||
document.getElementById('time').innerHTML = `${Y}-${m}-${d} ${H}:${M}:${S}`
|
|
||||||
|
|
||||||
error = false
|
|
||||||
// The same index.html is used for both coil and register data,
|
|
||||||
// change api url based on which we're looking at
|
|
||||||
if (document.location.pathname == "/coils/") {
|
|
||||||
url = "/api/v1/coils"
|
|
||||||
}
|
|
||||||
else if (document.location.pathname == "/registers/") {
|
|
||||||
url = "/api/v1/registers"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
document.getElementById("data").innerHTML = 'Page not found'
|
|
||||||
error = true
|
|
||||||
}
|
|
||||||
if (!error) {
|
|
||||||
// Fetch data from API
|
|
||||||
fetch(url)
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Error fetching data: ${response.status}`)
|
|
||||||
}
|
|
||||||
return response.json()
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
// Populate table
|
|
||||||
document.getElementById('coildata').innerHTML = "";
|
|
||||||
for (n=0; n<data.length; n++) {
|
|
||||||
tablerow = `<tr><td class="addr" id="addr_${data[n].address}">${data[n].address}</td>\
|
|
||||||
<td class ="val" id="value_${data[n].address}">${Number(data[n].value)}</td>\
|
|
||||||
<td class="symbol" id="symbol_${data[n].address}">${data[n].symbol}</td>\
|
|
||||||
<td class="desc" id="description_${data[n].address}">${data[n].description}</td></tr>`
|
|
||||||
document.getElementById('coildata').innerHTML += tablerow
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using setTimeout instead of setInterval to avoid possible connection issues
|
|
||||||
// There's no need to update exactly every 5 seconds, the skew is fine
|
|
||||||
setTimeout(getData, 1*1000);
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
upstream enervent-ctrl {
|
|
||||||
server localhost:8888;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
|
|
||||||
# SSL configuration
|
|
||||||
#
|
|
||||||
# listen 443 ssl default_server;
|
|
||||||
# listen [::]:443 ssl default_server;
|
|
||||||
#
|
|
||||||
# Note: You should disable gzip for SSL traffic.
|
|
||||||
# See: https://bugs.debian.org/773332
|
|
||||||
#
|
|
||||||
# Read up on ssl_ciphers to ensure a secure configuration.
|
|
||||||
# See: https://bugs.debian.org/765782
|
|
||||||
#
|
|
||||||
# Self signed certs generated by the ssl-cert package
|
|
||||||
# Don't use them in a production server!
|
|
||||||
#
|
|
||||||
# include snippets/snakeoil.conf;
|
|
||||||
|
|
||||||
root /home/jarno/enervent-ctrl/enervent-ctrl-python/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
# First attempt to serve request as file, then
|
|
||||||
# as directory, then fall back to displaying a 404.
|
|
||||||
if ($http_user_agent ~* "^curl") {
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
|
||||||
proxy_set_header X-Forwarded-Port $server_port;
|
|
||||||
proxy_pass http://enervent-ctrl;
|
|
||||||
}
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
#location ~ /static|/coils|/registers {
|
|
||||||
# root /home/jarno/enervent-ctrl/enervent-ctrl-python/html;
|
|
||||||
#}
|
|
||||||
|
|
||||||
location ~ /api {
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
|
||||||
proxy_set_header X-Forwarded-Port $server_port;
|
|
||||||
proxy_pass http://enervent-ctrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
home = /usr/bin
|
|
||||||
include-system-site-packages = false
|
|
||||||
version = 3.9.2
|
|
|
@ -1,11 +0,0 @@
|
||||||
click==8.1.3
|
|
||||||
Flask==2.2.2
|
|
||||||
importlib-metadata==6.0.0
|
|
||||||
itsdangerous==2.1.2
|
|
||||||
Jinja2==3.1.2
|
|
||||||
MarkupSafe==2.1.1
|
|
||||||
minimalmodbus==2.0.1
|
|
||||||
pyserial==3.5
|
|
||||||
waitress==2.1.2
|
|
||||||
Werkzeug==2.2.3
|
|
||||||
zipp==3.11.0
|
|
|
@ -1,188 +0,0 @@
|
||||||
import minimalmodbus
|
|
||||||
import logging
|
|
||||||
from flask import jsonify
|
|
||||||
from threading import Lock
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
class PingvinCoil():
|
|
||||||
"""Single coil data structure"""
|
|
||||||
def __init__(self, symbol="-", description="-"):
|
|
||||||
self.symbol = symbol
|
|
||||||
self.value = False
|
|
||||||
self.description = description
|
|
||||||
self.reserved = symbol == "-" and description == "-"
|
|
||||||
|
|
||||||
def serialize(self):
|
|
||||||
return {
|
|
||||||
"value": self.value,
|
|
||||||
"symbol": self.symbol,
|
|
||||||
"description": self.description,
|
|
||||||
"reserved": self.reserved
|
|
||||||
}
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return jsonify(self.serialize())
|
|
||||||
|
|
||||||
def flip(self):
|
|
||||||
self.value = not self.value
|
|
||||||
|
|
||||||
class PingvinCoils():
|
|
||||||
"""Class for handling Modbus coils"""
|
|
||||||
## coil descriptions and symbols courtesy of Ensto Enervent
|
|
||||||
## https://doc.enervent.com/out/out.ViewDocument.php?documentid=59
|
|
||||||
coils = [
|
|
||||||
PingvinCoil("COIL_STOP", "Stop"),
|
|
||||||
PingvinCoil("COIL_AWAY", "Away mode"),
|
|
||||||
PingvinCoil("COIL_AWAY_L", "Away Long mode"),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil("COIL_MAX_H", "Max Heating"),
|
|
||||||
PingvinCoil("COIL_MAX_C", "Max Cooling"),
|
|
||||||
PingvinCoil("COIL_CO_BOOST_EN", "CO2 boost"),
|
|
||||||
PingvinCoil("COIL_RH_BOOST_EN", "Relative humidity boost"),
|
|
||||||
PingvinCoil("COIL_M_BOOST", "Manual boost 100%"),
|
|
||||||
PingvinCoil("COIL_TEMP_BOOST_EN", "Temperature boost"),
|
|
||||||
PingvinCoil("COIL_SNC", "Summer night cooling"),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil("COIL_AWAY_H", "Heating enabled/disabled in AWAY mode"),
|
|
||||||
PingvinCoil("COIL_AWAY_C", "Cooling enabled/disabled in AWAY mode"),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil("COIL_LTO_ON", "Heat recycler state (running=1, stopped = 0)"),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil("COIL_HEAT_ON", "After heater element state (On = 1, Off = 0)"),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil("COIL_TEMP_DECREASE", "Temperature decrease function"),
|
|
||||||
PingvinCoil("COIL_OVERTIME", "Programmatic equivalent of OVERTIME digital input"),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil("COIL_ECO_MODE", "Eco mode"),
|
|
||||||
PingvinCoil("COIL_ALARM_A", "Alarm of class A active"),
|
|
||||||
PingvinCoil("COIL_ALARM_B", "Alarm of class B active"),
|
|
||||||
PingvinCoil("COIL_CLK_PROG", "Clock program is currently active"),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil("COIL_SILENT_MODE", "Silent mode"),
|
|
||||||
PingvinCoil("COIL_STOP_SLP_COOLING", "Electrical heater cool-off function enabled when the machine has stopped"),
|
|
||||||
PingvinCoil("COIL_SERVICE_EN", "Service reminder"),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil("COIL_COOLING_EN", "Active cooling function enabled"),
|
|
||||||
PingvinCoil("COIL_LTO_EN", "N/A"),
|
|
||||||
PingvinCoil("COIL_HEATING_EN", "Active heating function enabled"),
|
|
||||||
PingvinCoil("COIL_LTO_DEFROST_EN", "HRC defrosting function enabled during winter season"),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil(),
|
|
||||||
PingvinCoil()
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, device, semaphore, debug=False):
|
|
||||||
self.pingvin = device
|
|
||||||
self.semaphore = semaphore
|
|
||||||
|
|
||||||
def __getitem__(self, item):
|
|
||||||
return self.coils[item]
|
|
||||||
|
|
||||||
def update(self, debug=False):
|
|
||||||
"""Fetch all coils values from device"""
|
|
||||||
self.pingvin.serial.timeout = 0.2
|
|
||||||
self.pingvin.debug = debug
|
|
||||||
if debug: logging.info(f"{len(self.coils)} coils registered")
|
|
||||||
self.semaphore.acquire()
|
|
||||||
curvalues = self.pingvin.read_bits(0,len(self.coils),1)
|
|
||||||
self.semaphore.release()
|
|
||||||
for i, coil in enumerate(self.coils):
|
|
||||||
self.coils[i].value = bool(curvalues[i])
|
|
||||||
if debug: logging.info("Coil values read succesfully\n")
|
|
||||||
|
|
||||||
def fetchValue(self, address, debug=False):
|
|
||||||
"""Update single coil value from device and return it"""
|
|
||||||
self.pingvin.debug = debug
|
|
||||||
if debug: logging.debug("Updating coil value from device to cache")
|
|
||||||
self.semaphore.acquire()
|
|
||||||
self.coils[address].value = bool(self.pingvin.read_bit(address, 1))
|
|
||||||
self.semaphore.release()
|
|
||||||
return self.value(address, debug)
|
|
||||||
|
|
||||||
def value(self, address, debug=False):
|
|
||||||
"""Get single local coil value"""
|
|
||||||
if debug: logging.debug("Reading coil value from cache")
|
|
||||||
return self.coils[address].value
|
|
||||||
|
|
||||||
def print(self, debug=False):
|
|
||||||
"""Human-readable print of all coil values"""
|
|
||||||
coilvals = ""
|
|
||||||
for i, coil in enumerate(self.coils):
|
|
||||||
coilvals = coilvals + f"Coil {i : <{4}}{coil.value : <{2}} {coil.symbol : <{25}}{coil.description}\n"
|
|
||||||
return coilvals
|
|
||||||
|
|
||||||
def serialize(self, include_reserved=False):
|
|
||||||
"""Returns coil values as parseable Python object"""
|
|
||||||
coilvals = []
|
|
||||||
for i, coil in enumerate(self.coils):
|
|
||||||
if not coil.reserved or include_reserved:
|
|
||||||
coil = coil.serialize()
|
|
||||||
coil['address'] = i
|
|
||||||
coilvals.append(coil)
|
|
||||||
return coilvals
|
|
||||||
|
|
||||||
def get(self, include_reserved=False, live=False, debug=False):
|
|
||||||
"""Return all coil values in JSON format"""
|
|
||||||
if live: self.update(debug)
|
|
||||||
return jsonify(self.serialize(include_reserved))
|
|
||||||
|
|
||||||
def write(self, address):
|
|
||||||
self.semaphore.acquire()
|
|
||||||
self.pingvin.write_bit(address, int(not self.coils[address].value))
|
|
||||||
if self.pingvin.read_bit(address, 1) != self.coils[address].value:
|
|
||||||
self.coils[address].flip()
|
|
||||||
self.semaphore.release()
|
|
||||||
return True
|
|
||||||
self.semaphore.release()
|
|
||||||
return False
|
|
||||||
|
|
||||||
class PingvinKL():
|
|
||||||
"""Class for communicating with an Enervent Pinvin Kotilämpö ventilation/heating unit"""
|
|
||||||
def __init__(self, serialdevice='/dev/ttyS0', modbusaddr=1, debug=False):
|
|
||||||
self.semaphore = Lock()
|
|
||||||
self.pingvin = minimalmodbus.Instrument(serialdevice, modbusaddr)
|
|
||||||
self.coils = PingvinCoils(self.pingvin, self.semaphore, debug)
|
|
||||||
self.run = False
|
|
||||||
|
|
||||||
def monitor(self, interval=15, debug=False):
|
|
||||||
if not self.run: # Prevent starting two monitor threads
|
|
||||||
self.run = True
|
|
||||||
logging.info("Starting data monitor loop")
|
|
||||||
while self.run:
|
|
||||||
if debug: logging.info("Data monitor updating coil data")
|
|
||||||
self.coils.update(debug)
|
|
||||||
sleep(interval)
|
|
|
@ -1,47 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
import logging
|
|
||||||
from PingvinKL import PingvinKL
|
|
||||||
from flask import Flask, request
|
|
||||||
import threading
|
|
||||||
from waitress import serve
|
|
||||||
|
|
||||||
VERSION = "0.0.1"
|
|
||||||
DEBUG = False
|
|
||||||
|
|
||||||
## Logging configuration
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
if DEBUG:
|
|
||||||
dbglevel = logging.DEBUG
|
|
||||||
else:
|
|
||||||
dbglevel = logging.INFO
|
|
||||||
logging.basicConfig(
|
|
||||||
level=dbglevel,
|
|
||||||
format='%(asctime)s %(message)s',
|
|
||||||
datefmt='%y/%m/%d %H:%M:%S'
|
|
||||||
)
|
|
||||||
|
|
||||||
pingvin = PingvinKL('/dev/ttyS0',1,debug=DEBUG)
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
@app.route('/api/v1/coils')
|
|
||||||
def get_all():
|
|
||||||
return pingvin.coils.get(include_reserved=request.args.get('include_reserved'),live=request.args.get('live'),debug=DEBUG)
|
|
||||||
|
|
||||||
@app.route('/api/v1/coils/<int:address>', methods=["GET","PUT"])
|
|
||||||
def coil(address):
|
|
||||||
if request.method == 'GET':
|
|
||||||
coil = pingvin.coils[address].get()
|
|
||||||
return coil
|
|
||||||
elif request.method == 'PUT':
|
|
||||||
return {"success": pingvin.coils.write(address)}
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def dump():
|
|
||||||
return pingvin.coils.print(debug=DEBUG)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
log.info(f"Starting enervent-logger {VERSION}")
|
|
||||||
datathread = threading.Thread(target=pingvin.monitor, kwargs={"interval": 2, "debug": DEBUG})
|
|
||||||
datathread.start()
|
|
||||||
# app.run(host='0.0.0.0', port=8888)
|
|
||||||
serve(app, listen='*:8888', trusted_proxy='127.0.0.1', trusted_proxy_headers="x-forwarded-for x-forwarded-host x-forwarded-proto x-forwarded-port")
|
|
Loading…
Reference in New Issue