Readme.
"counter" file for historical total used.
Interface improvement: dynamic data with ajax, period selection,
favicon, config file.
init file for autostart.
--- /dev/null
+#!/usr/bin/php -f
+<?php
+include("html/config.php");
+
+exec("/usr/bin/rrdtool flushcached --daemon ".RRDSOCK." ".RRDFILE);
+
+$fd = fopen(COUNTERFILE, 'r');
+$mtime = fstat($fd)['mtime'];
+$counter = (double)fread($fd, 128);
+fclose($fd);
+echo("oldcounter=$counter\n");
+
+$params = [
+ '--daemon', RRDSOCK,
+ '-s', $mtime,
+ 'DEF:watts='.RRDFILE.':watts:AVERAGE',
+ 'VDEF:avg=watts,AVERAGE',
+ 'PRINT:avg:%lf',
+];
+$info=rrd_graph( '-', $params);
+$watts_since_counter = (double)$info['calcpr'][0];
+echo("watts_since_counter=$watts_since_counter\n");
+echo('hours_since_counter='.((time() - $mtime) / 3600.)."\n");
+$wh_since_counter = $watts_since_counter * (time() - $mtime) / 3600.;
+echo("wh_since_counter=$wh_since_counter\n");
+$newcounter = $counter + $wh_since_counter / 1000.;
+echo('newcounter='.$newcounter." kWh\n");
+
+file_put_contents(COUNTERFILE.'.new', $newcounter);
+rename(COUNTERFILE.'.new', COUNTERFILE);
+?>
--- /dev/null
+<?php
+$RRDFILE='/home/nirgal/ampy/power.rrd';
+$COUNTERFILE='/home/nirgal/ampy/counter';
+
+function get_counter() {
+ $fd = fopen($COUNTERFILE, 'r');
+ $mtime = fstat($fd)['mtime'];
+ $counter = (double)fread($fd, 128) * 1000;
+ fclose($fd);
+ #echo($mtime.'<br>');
+ #echo('counter='.$counter.'<br>');
+
+ $info=rrd_graph( '-', ['-s', $mtime, 'DEF:watts='.$RRDFILE.':watts:AVERAGE', 'VDEF:avg=watts,AVERAGE', 'PRINT:avg:%lf']);
+ #print_r($info);
+ $watts_since_counter = (double)$info['calcpr'][0];
+ #echo('watts_since_counter='.$watts_since_counter.'<br>');
+ #echo('hours_since_counter='.((time() - $mtime) / 3600.).'<br>');
+ $wh_since_counter = $watts_since_counter * (time() - $mtime) / 3600.;
+ #echo('wh_since_counter='.$wh_since_counter.'<br>');
+ $counter += $wh_since_counter;
+ #echo('newcounter='.($counter/1000).' kWh<br>');
+
+ return $counter/1000.;
+}
+?>
--- /dev/null
+<?php
+define("RRDSOCK", "/var/run/rrdcached.sock");
+define("RRDFILE", "/var/lib/rrdcached/db/power.rrd");
+define("COUNTERFILE", "/var/lib/ampy/counter");
+define("IMGWIDTH", 970);
+define("IMGHEIGHT", 490);
+?>
<?php
-$RRDFILE="/home/nirgal/ampy/power.rrd";
-$info=rrd_lastupdate($RRDFILE);
+require("config.php");
+
+exec("/usr/bin/rrdtool flushcached --daemon ".RRDSOCK." ".RRDFILE);
+
+$fd = fopen(COUNTERFILE, 'r');
+$mtime = fstat($fd)['mtime'];
+$counter = (double)fread($fd, 128) * 1000;
+fclose($fd);
+#echo($mtime.'<br>');
+#echo('counter='.$counter.'<br>');
+
+$params = [
+ '--daemon', RRDSOCK,
+ '-s', $mtime,
+ 'DEF:watts='.RRDFILE.':watts:AVERAGE',
+ 'VDEF:avg=watts,AVERAGE',
+ #'VDEF:last=watts,LAST',
+ 'PRINT:avg:%lf',
+ #'PRINT:last:%lf',
+];
+/*header("Content-Type: text/plain");
+foreach ($params as $param) echo(escapeshellarg($param)." ");
+return;*/
+$info=rrd_graph( '-', $params);
+#print_r($info);
+$watts_since_counter = (double)$info['calcpr'][0];
+#$watts_last = (double)$info['calcpr'][1];
+#echo('watts_since_counter='.$watts_since_counter.'<br>');
+#echo('hours_since_counter='.((time() - $mtime) / 3600.).'<br>');
+$wh_since_counter = $watts_since_counter * (time() - $mtime) / 3600.;
+#echo('wh_since_counter='.$wh_since_counter.'<br>');
+$newcounter = $counter + $wh_since_counter;
+#echo('newcounter='.$newcounter.' kWh<br>');
+
+$info=rrd_lastupdate(RRDFILE);
if ($info === FALSE)
$result=[
'last_update' => 'unknown',
else
$result=[
'last_update' => $info['last_update'],
- 'watts' => $info['data'][0]
+ 'watts' => (double)$info['data'][0]
];
+
+#$result=[];
+#$result['watts'] = $watts_last;
+$result['counter'] = $newcounter / 1000.;
+
header("Content-Type: text/json");
echo(json_encode($result));
?>
--- /dev/null
+<?php
+$RRDFILE="/home/nirgal/ampy/power.rrd";
+
+header("Content-Type: text/json");
+header("Transfer-Encoding: chunked");
+flush();
+
+
+// Send chunk to browser
+function send_chunk($chunk)
+{
+ // The chunk must fill the output buffer or php won't send it
+ $chunk = str_pad($chunk, 4096);
+
+ printf("%x\r\n%s\r\n", strlen($chunk), $chunk);
+ flush();
+}
+
+while (True) {
+ $info=rrd_lastupdate($RRDFILE);
+ if ($info === FALSE)
+ $result=[
+ 'last_update' => 'unknown',
+ 'watts' => 'unknown'
+ ];
+ else
+ $result=[
+ 'last_update' => $info['last_update'],
+ 'watts' => $info['data'][0]
+ ];
+ send_chunk(json_encode($result));
+ sleep(10);
+}
+?>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Notepad -->
+<!-- (PD) January 25, 2006 - _Crotalus horridus_ -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" width="40" height="40" id="Stylized Lightning Bolt on Circle">
+ <defs>
+ <radialGradient id="CircleGradient" gradientUnits="userSpaceOnUse" cx="20" cy="20" fx="20" fy="20" r="13">
+ <stop offset="0%" stop-color="#FFFF00" stop-opacity="1.0"/>
+ <stop offset="100%" stop-color="#FFFFFF" stop-opacity="0.0"/>
+ </radialGradient>
+ </defs>
+ <ellipse fill="#FF8000" stroke="#000000" stroke-width="1" cx="20" cy="20" rx="13" ry="13" id="Background Circle"/>
+ <ellipse fill="url(#CircleGradient)" fill-opacity="1.0" cx="20" cy="20" rx="13" ry="13" id="Shading Circle"/>
+ <path fill="#FFFF00" stroke="#000000" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" d="M 36,4 L 12,16 18,21 4,36 28,24 22,19 Z" id="Lightning Bolt"/>
+</svg>
\ No newline at end of file
<?php
-$RRDFILE="/home/nirgal/ampy/power.rrd";
+
+require("config.php");
+
$IMGFILE="/tmp/power.svg";
-$REFRESH=30;
-$result=rrd_graph("$IMGFILE", [
+$hours = array_key_exists('h', $_REQUEST) ? (int)$_REQUEST['h'] : 0;
+$minutes = array_key_exists('m', $_REQUEST) ? (int)$_REQUEST['m'] : 0;
+if (!$hours && !$minutes)
+ $hours = 1;
+$params = [
+ "--daemon", RRDSOCK,
"--imgformat", "SVG",
- "-s", "now-1h",
- #"-e", "now-8h",
- "--width", "1000",
- "--height", "500",
+ "-s", "now-".$hours."h".$minutes."m",
+ "--width", "".IMGWIDTH,
+ "--height", "".IMGHEIGHT,
"--vertical-label", "watts",
"--lower-limit", "0",
- #"--logarithmic",
- "DEF:watts=${RRDFILE}:watts:AVERAGE",
- "LINE1:watts#FF0000"
-]);
+ "DEF:watts=".RRDFILE.":watts:AVERAGE",
+ #"DEF:maxwatts=".RRDFILE.":watts:MAX:step=".(60*(60*$hours)+$minutes),
+ #"DEF:maxwatts=".RRDFILE.":watts:MAX",
+ "CDEF:wh_raw=watts,".(float)($hours+$minutes/60.).",*",
+ "VDEF:watts_avg=watts,AVERAGE",
+ "VDEF:wh=wh_raw,AVERAGE",
+ "AREA:watts#FF0000",
+ "LINE:watts_avg#00FF00",
+ "GPRINT:watts_avg:Average %6.2lf %swatts",
+ "GPRINT:wh:Total %6.2lf %swh",
+ #"LINE:maxwatts#0000FF",
+ #"GPRINT:maxwatts:Max %6.2lf %sW",
+];
+
+/*header("Content-Type: text/plain");
+foreach ($params as $param) echo(escapeshellarg($param)." ");
+return;*/
+
+$result=rrd_graph("$IMGFILE", $params);
if ($result === FALSE) {
- echo("Error in rrd_graph");
+ echo("Error in rrd_graph:<br>");
+ echo(rrd_error());
die();
}
header("Content-Type: image/svg+xml");
-#header("Refresh: $REFRESH");
+header("Cache-Control: no-cache, max-age=0, must-revalidate");
+header("Refresh: ".(60*(60*$hours)+$minutes)/IMGWIDTH);
readfile($IMGFILE);
?>
<!DOCTYPE html>
<meta charset="UTF-8">
+<link rel="shortcut icon" href="favicon.svg">
+<title>ampy - Le compteur espion qui ne moucharde pas</title>
<script src='/javascript/jquery/jquery.js'></script>
<script>
function refresh_power() {
$.ajax({
url : 'currentjson.php',
success : function(result) {
- document.getElementById('currentpower').innerHTML = result['watts'];
+ document.getElementById('currentpower').innerHTML = result['watts'].toFixed(1);
+ document.getElementById('totalpower').innerHTML = result['counter'].toFixed(5);
},
complete : function() {
setTimeout(refresh_power, 1000);
}
});
}
+function refresh_graph(hours=0, minutes=0) {
+ img = document.getElementById('loadcurve');
+ img.src = 'graph.php?h='+hours+'&m='+minutes+'&t='+new Date().getTime();
+}
</script>
-Puissance instannée: <span id=currentpower>unknown</span> watts<br>
+Puissance instantannée: <span id=currentpower>unknown</span> watts
<script>refresh_power();</script>
+-
+
+Compteur: <span id=totalpower>unknown</span> kWh
+
+-
+
+Graphique:
+<a href='javascript:refresh_graph(0,5)'>5m</a>
+<a href='javascript:refresh_graph(0,30)'>30m</a>
+<a href='javascript:refresh_graph(1)'>1h</a>
+<a href='javascript:refresh_graph(2)'>2h</a>
+<a href='javascript:refresh_graph(6)'>6h</a>
+<a href='javascript:refresh_graph(12)'>12h</a>
+<a href='javascript:refresh_graph(24)'>1d</a>
+<a href='javascript:refresh_graph(2*24)'>2d</a>
+<a href='javascript:refresh_graph(7*24)'>7d</a>
+<a href='javascript:refresh_graph(30*24)'>30d</a>
+
+<br>
-<img src="graph.php">
+<img id='loadcurve' src='graph.php'>
--- /dev/null
+<!DOCTYPE html>
+<meta charset="UTF-8">
+<script src='/javascript/jquery/jquery.js'></script>
+
+<hr>
+<span id=debugg>
+</span>
+<hr>
+
+Puissance instannée: <span id=currentpower>Unknown</span> watts<br>
+
+<script>
+xhr = new XMLHttpRequest();
+xhr.onreadystatechange = function() {
+ document.getElementById('debugg').innerHTML += ""+xhr.readyState+"<br>";
+ document.getElementById('debugg').innerHTML += ""+xhr.status+" "+xhr.statusText+"<br>";
+ document.getElementById('debugg').innerHTML += ""+xhr.responseText+"<br>";
+ this.response = "";
+}
+xhr.open('GET', 'currentjson_chunked.php', true /*async*/);
+xhr.send(null);
+
+//autorefresh_power = 1;
+//function refresh_power() {
+// $.ajax({
+// url : 'currentjson_chunked.php',
+// success : function(result) {
+// document.getElementById('debugg').innerHTML = document.getElementById('debugg').innerHTML + "success<br>";
+// alert(result);
+// document.getElementById('currentpower').innerHTML = result['watts'];
+// },
+// complete : function(textStatus) {
+// document.getElementById('debugg').innerHTML = document.getElementById('debugg').innerHTML + "complete<br>";
+// // if (autorefresh_power)
+// // setTimeout("refresh_power()", 1000);
+// }
+// });
+//}
+//refresh_power();
+</script>
+
+
+<img src="graph.php">
--- /dev/null
+<!DOCTYPE html>
+<meta charset="UTF-8">
+
+<?php
+$RRDFILE="/home/nirgal/ampy/power.rrd";
+
+$info=rrd_lastupdate($RRDFILE);
+if ($info === FALSE)
+ echo("error in rrd_lastupdate");
+else
+ print_r($info);
+?>
--- /dev/null
+#!/bin/sh
+### BEGIN INIT INFO
+# Provides: ampy
+# Required-Start: $remote_fs
+# Required-Stop: $remote_fs
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: ampy
+# Description: Listen to ampmeter on microphone and fill in an rrd file
+### END INIT INFO
+
+PATH=/bin:/usr/bin:/sbin:/usr/sbin
+PIDFILE=/var/run/ampy.pid
+USER=nirgal
+GROUP=ampy
+PROG=/usr/bin/python3
+ARGS=/home/nirgal/ampy/run.py
+NICE=-10
+
+set -e
+
+. /lib/lsb/init-functions
+
+d_start () {
+ log_daemon_msg "Starting ampmeter recording" "ampy"
+ if start-stop-daemon --start --quiet --background --exec $PROG --user $USER --pidfile $PIDFILE --chuid $USER:$GROUP --umask 002 --make-pidfile --nicelevel $NICE -- $ARGS
+ then
+ log_success_msg
+ else
+ log_failure_msg
+ fi
+}
+
+d_stop () {
+ log_daemon_msg "Stopping ampmeter recording" "ampy"
+ if start-stop-daemon --stop --quiet --exec $PROG --user $USER --pidfile $PIDFILE -- $ARGS
+ then
+ log_success_msg
+ else
+ log_failure_msg
+ fi
+}
+
+case $1 in
+ start)
+ d_start
+ ;;
+ stop)
+ d_stop
+ ;;
+ restart|force-reload)
+ d_stop
+ d_start
+ ;;
+ *)
+ log_success_msg "Usage: ampy {start|stop|restart|force-reload}"
+ exit 1
+ ;;
+esac
https://learn.openenergymonitor.org/electricity-monitoring/ct-sensors/yhdc-sct-013-000-ct-sensor-report
-rrdtool create power.rrd --start now-2h --step 1s DS:watts:GAUGE:1:0:6600 RRA:AVERAGE:0.5:1s:1d # Buggy: nécessite des updates toutes les 500 ms
+rrdtool create power.rrd --step 1s DS:watts:GAUGE:1:0:6600 RRA:AVERAGE:0.5:1s:1d # Buggy: nécessite des updates toutes les 500 ms
+rrdtool create power.rrd --daemon /var/run/rrdcached.sock --step 1s DS:watts:GAUGE:1:0:6600 RRA:AVERAGE:0.5:1s:1d # Buggy: nécessite des updates toutes les 500 ms
+
+rrdtool create /var/lib/rrdcached/db/power.rrd \
+ --step 1s \
+ DS:watts:GAUGE:1s:0:10000 \
+ RRA:AVERAGE:0.5:1s:1d \
+ RRA:AVERAGE:0.5:1m:3d \
+ RRA:AVERAGE:0.5:15m:1w \
+ RRA:AVERAGE:0.5:1h:30d \
+ RRA:AVERAGE:0.5:1d:3y \
+ RRA:MAX:0.5:1d:3y \
+ RRA:AVERAGE:0.5:1month:10y \
+ RRA:MAX:0.5:1month:10y
+
+Ceci n'est pas rafraîchi très souvent:
+watch -n 0.5 rrdtool graphv - '-s' '1546280606' 'DEF:watts=/var/lib/rrdcached/db/power.rrd:watts:AVERAGE' 'VDEF:avg=watts,AVERAGE' 'VDEF:last=watts,LAST' 'PRINT:avg:%lf' 'PRINT:last:%lf'
+Alors que ça si:
+watch -n 0.5 rrdtool lastupdate --daemon /var/run/rrdcached.sock /var/lib/rrdcached/db/power.rrd
+#!/usr/bin/python3
"""
PyAudio example: Record a few seconds of audio and save to a WAVE
file.
import wave
import sys
import struct
+import time
+FORMAT = pyaudio.paInt32 # Format
+STRUCTFORMAT = 'i' # Format for python struct module
DEVICE = 3 # which alsa decive to read
-CHUNK = 1024 # how many bytes at a time?
-FORMAT = pyaudio.paInt16 # Format
RATE = 44100
RECORD_SECONDS = 0.5
-VOLUME_CONSTANT = 11.975454545454545
-
-p = pyaudio.PyAudio()
-
-stream = p.open(format=FORMAT,
- channels=1, # Our ampmeter always returns 0 on the second channel
- rate=RATE,
- input=True,
- input_device_index=DEVICE,
- frames_per_buffer=CHUNK)
-
-while(True):
- total = 0
- for i in range(0, int(RATE * RECORD_SECONDS / CHUNK)):
- data = stream.read(CHUNK, exception_on_overflow=False)
- values = struct.unpack('h'*CHUNK, data)
- #print(values)
- chunk_total = 0
- for value in values:
- if value > 0:
- chunk_total += value
- else:
- chunk_total -= value
- chunk_total /= len(values)
- #print(chunk_total)
- total += chunk_total
- avg = total / int(RATE * RECORD_SECONDS / CHUNK)
- watts = avg / VOLUME_CONSTANT
- print("average {:.0f}W".format(watts))
- os.system("rrdtool update power.rrd N:{:.0f}".format(watts))
-
-stream.stop_stream()
-stream.close()
-p.terminate()
+VOLUME_CONSTANT = 132360.98315789475
+#amixer -c 1 contents
+# numid=23,iface=MIXER,name='Capture Volume'
+# ; type=INTEGER,access=rw---R--,values=2,min=0,max=46,step=0
+# : values=17,17
+# | dBscale-min=-16.00dB,step=1.00dB,mute=0
+# => Set volume to 18
+
+# https://www.actutem.com/valeur-crete-moyenne-et-efficace-dune-tension-ac/
+# math.pi/math.sqrt(2)/2 = 1.1107207345395915
+
+# Pour 30 A à 230V ( 6900 W )
+# abs(Sndmax) = 2147483520
+# Sndavg = 2147483520/math.pi = 683565234.8327662
+# Sndeff = 683565234.8327662 * 1.1107207345395915 = 759250079.7391784
+
+# https://fr.wikipedia.org/wiki/Compteur_%C3%A9lectrique
+# Mon compteur tourne à une vitesse proportionnelle à la puissance instantanée
+
+# https://fr.wikipedia.org/wiki/%C3%89lectricit%C3%A9_domestique#Tension
+# En france, ERDF fourni 230V EFFICACES (=> 207.0727527161344 moyenne)
+
+#raw average: 636207384.4738322 - min: -2147483648 - max: 2147483392
+#average 4806.6W
+
+def loop(optrecord, optstats):
+ p = pyaudio.PyAudio()
+
+ print("opened")
+ NSAMPLE = int(RATE * RECORD_SECONDS)
+ stream = p.open(format=FORMAT,
+ channels=1, # Our ampmeter always returns 0 on the second channel
+ rate=RATE,
+ input=True,
+ input_device_index=DEVICE)
+
+ structformat = '<' + STRUCTFORMAT * NSAMPLE
+ while(True):
+ try:
+ total = 0
+ minvalue = 0
+ maxvalue = 0
+ data = stream.read(NSAMPLE, exception_on_overflow=False)
+ values = struct.unpack(structformat, data)
+ #print(values)
+ #for x in range(100): print('**{:04x}**{}**'.format(values[x], values[x]))
+ for value in values:
+ if value > 0:
+ total += value
+ else:
+ total -= value
+ if value > maxvalue:
+ maxvalue = value
+ if value < minvalue:
+ minvalue = value
+ avg = float(total) / NSAMPLE
+ if optstats:
+ print("raw average: {} - min: {} - max: {}".format(avg, minvalue, maxvalue))
+ watts = avg / VOLUME_CONSTANT
+ print("average {:.1f}W ".format(watts), end='\r')
+ if optrecord:
+ #os.system("rrdtool update --daemon /var/run/rrdcached.sock power.rrd N:{}".format(watts))
+ os.system("rrdtool update --daemon /var/run/rrdcached.sock /var/lib/rrdcached/db/power.rrd {}:{}".format(time.time(), watts))
+ except KeyboardInterrupt:
+ print("Received KeyboardInterrupt: exiting")
+ break
+
+ stream.stop_stream()
+ stream.close()
+ p.terminate()
+
+def main():
+ import argparse
+
+ parser = argparse.ArgumentParser(description='Ampemeter processing')
+ parser.add_argument(
+ '--stats',
+ action='store_true',
+ default=False,
+ help='Dump recording level stats for every chunk',
+ )
+ parser.add_argument(
+ '--norecord',
+ action='store_false',
+ default=True,
+ help='Disable recording in RRD file',
+ dest='record',
+ )
+ args = parser.parse_args()
+
+ if not args.record:
+ print('Recording disabled.')
+
+ loop(optrecord=args.record, optstats=args.stats)
+
+if __name__ == '__main__':
+ main()