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