Initial commit.

This commit is contained in:
yohan 2024-02-04 21:19:01 +01:00
commit 90e2d074ba
6 changed files with 272 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
conf.yml

26
conf-example.yml Normal file
View File

@ -0,0 +1,26 @@
http_port: 3000
polling_conf:
- name: teleinfo
metrics:
- name: Modane_elec_main_power
type: int
- name: Modane_elec_energy_index
type: int
script: ./get-teleinfo.py
arguments:
- "-f"
- "custom_json"
# interval between sensors polling in seconds
polling_interval: 10.0
# Default interval between sending to record API in seconds
default_recording_interval: 600.0
# Default interval between sensors polling in seconds
default_polling_interval: 600.0
recording_api_key: "FIXME"
post_url:
int: "http://ovh1.scimetis.net:3001/integer_metric/add"
float: "http://ovh1.scimetis.net:3001/float_metric/add"

37
get-teleinfo.py Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# # pip install teleinfo
# https://pypi.org/project/teleinfo/
# https://www.magdiblog.fr/gpio/teleinfo-edf-suivi-conso-de-votre-compteur-electrique/
import argparse
import json
from teleinfo import Parser
from teleinfo.hw_vendors import UTInfo2
parser = argparse.ArgumentParser(description='Téléinfo retriever.')
parser.add_argument("-f", "--format", help="Output format.",
type=str, choices=['human-readable', 'raw_json', 'custom_json'], default='human-readable')
args = parser.parse_args()
ti = Parser(UTInfo2(port="/dev/ttyUSB0"))
res = ti.get_frame()
if args.format == 'human-readable':
print "Puissance apparente compteur : "+str(int(res['PAPP']))+"VA"
# moins précis car Intensité arrondie à l'entier
print "Puissance apparente calculée : "+str(int(res['IINST'])*230)+"VA"
print "Puissance souscrite : 6kVA"
print "Puissance max avant coupure (marge 30%) : 7,8kVA"
print "Intensité : "+str(int(res['IINST']))+"A"
print "Intensité abonnement : "+str(int(res['ISOUSC']))+"A"
print "Consommation : "+str(int(res['BASE']))+"Wh"
elif args.format == 'raw_json':
print json.dumps(res)
elif args.format == 'custom_json':
data = {}
data['Modane_elec_main_power'] = int(res['PAPP'])
data['Modane_elec_energy_index'] = int(res['BASE'])
print json.dumps(data)
#for frame in ti:
# print frame

25
install.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
#Absolute path to this script
SCRIPT=$(readlink -f $0)
#Absolute path this script is in
SCRIPTPATH=$(dirname $SCRIPT)
SERVICE="sensors-polling"
cat << EOF > /etc/systemd/system/${SERVICE}.service
[Unit]
Description=Starting ${SERVICE}
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=$SCRIPTPATH/${SERVICE}.py
WorkingDirectory=$SCRIPTPATH
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable ${SERVICE}.service

1
requirements Normal file
View File

@ -0,0 +1 @@
apt install python3 python3-requests python3-yaml

182
sensors-polling.py Executable file
View File

@ -0,0 +1,182 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import logging
import time
import signal
import yaml
import requests
import subprocess
import argparse
import threading
import socketserver
from http.server import BaseHTTPRequestHandler
from threading import Event
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor
parser = argparse.ArgumentParser(description='Sensors polling and metrics recording.')
parser.add_argument("-v", "--verbosity", help="Increase output verbosity",
type=str, choices=['DEBUG', 'INFO', 'WARNING'], default='INFO')
args = parser.parse_args()
if args.verbosity == 'DEBUG':
logging.basicConfig(level=logging.DEBUG)
elif args.verbosity == 'INFO':
logging.basicConfig(level=logging.INFO)
elif args.verbosity == 'WARNING':
logging.basicConfig(level=logging.WARNING)
logging.info("====== Starting ======")
stop = Event()
last_data = {}
def handler(signum, frame):
global stop
logging.info("Got interrupt: "+str(signum))
stop.set()
logging.info("Shutdown")
signal.signal(signal.SIGTERM,handler)
signal.signal(signal.SIGINT,handler)
with open('./conf.yml') as conf:
yaml_conf = yaml.load(conf)
polling_conf = yaml_conf.get("polling_conf")
http_port = yaml_conf.get("http_port")
default_polling_interval = yaml_conf.get("default_polling_interval")
default_recording_interval = yaml_conf.get("default_recording_interval")
max_threads = len(polling_conf)
recording_api_key = yaml_conf.get("recording_api_key")
post_url = yaml_conf.get("post_url")
def sensors_polling(poller_conf):
global stop
global last_data
s = requests.Session()
start_time=time.time()
last_polling_time=None
last_recording_time=None
if 'polling_interval' in poller_conf.keys():
polling_interval = poller_conf['polling_interval']
else:
polling_interval = default_polling_interval
if 'recording_interval' in poller_conf.keys():
recording_interval = poller_conf['recording_interval']
else:
recording_interval = default_recording_interval
while True:
if stop.is_set():
logging.info('Stopping thread '+poller_conf['name'])
break
logging.debug('New while loop for '+poller_conf['name'])
utc_now = datetime.utcnow()
now = datetime.now()
current_time=time.time()
# Polling
try:
logging.debug('Getting data for '+poller_conf['name'])
command = [poller_conf['script']] + poller_conf['arguments']
returned_output = subprocess.check_output(command)
data = json.loads(returned_output.decode("utf-8"))
logging.debug('Got: '+returned_output.decode("utf-8"))
for metric in poller_conf['metrics']:
last_data[metric['name']] = {'value': data[metric['name']], 'timestamp': utc_now.isoformat()}
last_polling_time=time.time()
except Exception as e:
logging.error(e)
if last_polling_time is None:
polling_missed = int((current_time - start_time) // polling_interval)
else:
polling_missed = int((current_time - last_polling_time) // polling_interval)
if polling_missed > 0:
logging.warning("Missed "+str(polling_missed)+" polling iteration(s)")
# Recording
if last_polling_time is not None and (last_recording_time is None or (current_time - last_recording_time > recording_interval and last_polling_time > last_recording_time + recording_interval/2)):
try:
for metric in poller_conf['metrics']:
logging.debug('Posting data for '+metric['name'])
r = s.post(post_url[metric['type']],
headers={'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0',
'X-API-KEY': recording_api_key},
json={'metric': metric['name'],
'value': last_data[metric['name']]['value'],
'time': utc_now.isoformat()})
if r.status_code != 201:
logging.error(str(r.status_code)+" "+r.reason)
last_recording_time=time.time()
except Exception as e:
logging.error(e)
if last_recording_time is None:
recording_missed = int((current_time - start_time) // recording_interval)
else:
recording_missed = int((current_time - last_recording_time) // recording_interval)
if recording_missed > 0:
logging.warning("Missed "+str(recording_missed)+" recording iteration(s)")
# Sleeping
time_to_sleep = polling_interval - ((current_time - start_time) % polling_interval)
logging.debug('Sleeping '+str(time_to_sleep)+' seconds for '+poller_conf['name'])
stop.wait(timeout=time_to_sleep)
def metric_list():
return([metric['name'] for metric in poller_conf['metrics']])
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/':
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(bytes(str(metric_list())+'\n', 'utf-8'))
if self.path[1:] in metric_list():
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(bytes(json.dumps(last_data[self.path[1:]])+'\n', 'utf-8'))
else:
self.send_response(404)
class WebThread(threading.Thread):
def run(self):
httpd.serve_forever()
httpd = socketserver.TCPServer(("", http_port), MyHandler, bind_and_activate=False)
httpd.allow_reuse_address = True
httpd.server_bind()
httpd.server_activate()
webserver_thread = WebThread()
webserver_thread.start()
executor = ThreadPoolExecutor(max_workers=max_threads)
threads = []
for poller_conf in polling_conf:
threads.append(executor.submit(sensors_polling, poller_conf))
logging.info("Polling "+str(metric_list()))
while True:
if stop.is_set():
executor.shutdown(wait=True)
httpd.shutdown()
httpd.server_close()
break
for thread in threads:
if not thread.running():
try:
res = thread.exception(timeout=1)
if res is not None:
logging.error(res)
except Exception as e:
logging.error(e)
stop.wait(timeout=0.5)
logging.info("====== Ended successfully ======")
# vim: set ts=4 sw=4 sts=4 et :