first commit
This commit is contained in:
commit
2a16f8e5f1
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/target
|
||||
/dependency-reduced-pom.xml
|
||||
/docker-compose.yml
|
||||
/workdir
|
3
.idea/.gitignore
vendored
Normal file
3
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
17
.idea/compiler.xml
Normal file
17
.idea/compiler.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile name="Maven default annotation processors profile" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<module name="tic" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
<bytecodeTargetLevel>
|
||||
<module name="tic-lib" target="1.8" />
|
||||
<module name="tic-main" target="1.8" />
|
||||
</bytecodeTargetLevel>
|
||||
</component>
|
||||
</project>
|
9
.idea/encodings.xml
Normal file
9
.idea/encodings.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/lib/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/lib/src/main/resources" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
2550
.idea/inspectionProfiles/Project_Default.xml
Normal file
2550
.idea/inspectionProfiles/Project_Default.xml
Normal file
File diff suppressed because it is too large
Load Diff
20
.idea/jarRepositories.xml
Normal file
20
.idea/jarRepositories.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Central Repository" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
17
.idea/misc.xml
Normal file
17
.idea/misc.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="ignoredFiles">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$/lib/pom.xml" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" />
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@ -0,0 +1,16 @@
|
||||
FROM arm32v6/openjdk:8u212-jdk-alpine
|
||||
|
||||
|
||||
ADD run.sh target/tic.jar /data/bin/
|
||||
|
||||
RUN chmod u+x /data/bin/*
|
||||
|
||||
ENV TIC_MQTT_SERVER_IP="127.0.0.1"
|
||||
ENV TIC_MQTT_HASS_DISCOVERY_PREFIX="homeassistant"
|
||||
ENV TIC_MQTT_USERNAME="name"
|
||||
ENV TIC_MQTT_PASSWORD="pass"
|
||||
ENV TIC_TTY_INPUT="/dev/ttyAMA0"
|
||||
|
||||
WORKDIR /data/workdir
|
||||
|
||||
ENTRYPOINT /data/bin/run.sh -i
|
16
docker-compose-example.yml
Normal file
16
docker-compose-example.yml
Normal file
@ -0,0 +1,16 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
tic:
|
||||
build: .
|
||||
restart: always
|
||||
environment:
|
||||
TIC_MQTT_SERVER_IP: "mqtt_ip"
|
||||
TIC_MQTT_HASS_DISCOVERY_PREFIX: "homeassistant"
|
||||
TIC_MQTT_USERNAME: "username"
|
||||
TIC_MQTT_PASSWORD: "password"
|
||||
TIC_TTY_INPUT: "/dev/ttyAMA0"
|
||||
devices:
|
||||
- "/dev/ttyAMA0:/dev/ttyAMA0"
|
||||
volumes:
|
||||
- "./workdir:/data/workdir"
|
80
pom.xml
Normal file
80
pom.xml
Normal file
@ -0,0 +1,80 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>fr.mbaloup</groupId>
|
||||
<artifactId>tic</artifactId>
|
||||
<version>dev</version>
|
||||
<name>tic</name>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>1.2.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.rapidoid</groupId>
|
||||
<artifactId>rapidoid-essentials</artifactId>
|
||||
<version>5.5.5</version>
|
||||
</dependency>
|
||||
<!-- <dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>1.7.21</version>
|
||||
<optional>true</optional>
|
||||
</dependency> -->
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>${project.name}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>2.4</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Main-Class>fr.mbaloup.home.Main</Main-Class>
|
||||
<Specification-Version>${maven.build.timestamp}</Specification-Version>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>2.4.3</version>
|
||||
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
135
res/static/index.css
Normal file
135
res/static/index.css
Normal file
@ -0,0 +1,135 @@
|
||||
* { box-sizing: border-box; }
|
||||
html, input { font-family: Arial, Helvetica, sans-serif; }
|
||||
html, body { margin: 0; padding: 0;}
|
||||
:root {
|
||||
--color-theme: rgb(3.921%, 39.21%, 68.23%);
|
||||
--min-size: calc(min(calc(100vw / 0.6), 100vh) / 3);
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
border: solid 1px var(--color-theme);
|
||||
border-radius: 2px;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.widget > header {
|
||||
background-color: var(--color-theme);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.widget > main {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media screen and (min-width: 800px) {
|
||||
body {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#left-widget-container {
|
||||
width: 400px;
|
||||
}
|
||||
#right-widget-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#login-form .widget {
|
||||
margin: 16px auto;
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 799.99px) {
|
||||
.widget {
|
||||
margin: 16px auto;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.raw-value {
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
font-size: 1.3em;
|
||||
color: var(--color-theme);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.big-value {
|
||||
text-align: right;
|
||||
position: relative;
|
||||
}
|
||||
.big-value .name {
|
||||
position: absolute;
|
||||
top: 0.2em; left: 0;
|
||||
}
|
||||
.big-value .int-value {
|
||||
font-size: 3.5em;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
color: var(--color-theme);
|
||||
}
|
||||
.big-value .dec-value {
|
||||
font-size: 1.6em;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
color: var(--color-theme);
|
||||
}
|
||||
.big-value .unit {
|
||||
position: absolute;
|
||||
top: 0.2em; right: 0;
|
||||
}
|
||||
|
||||
.big-value .meta {
|
||||
position: absolute;
|
||||
bottom: 1em; left: 0;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.live-data-index:not(.index-current) {
|
||||
opacity: 33%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
form input[type="text"],
|
||||
form input[type="password"],
|
||||
form input[type="email"],
|
||||
form input[type="number"],
|
||||
form input[type="date"],
|
||||
form input[type="time"],
|
||||
form textarea,
|
||||
form select {
|
||||
display: block;
|
||||
margin: 5px auto;
|
||||
border: solid 1px var(--color-theme);
|
||||
border-radius: 2px;
|
||||
padding: 5px;
|
||||
font-size: 1.1em;
|
||||
width: 100%;
|
||||
}
|
||||
form input[type="submit"] {
|
||||
display: block;
|
||||
margin: 5px auto;
|
||||
border: solid 1px var(--color-theme);
|
||||
border-radius: 2px;
|
||||
padding: 5px;
|
||||
font-size: 1.1em;
|
||||
width: 100%;
|
||||
}
|
157
res/static/index.html
Normal file
157
res/static/index.html
Normal file
@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Domotique</title>
|
||||
<link rel="stylesheet" type="text/css" href="index.css" media="screen"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<div id="logged-in-content" style="display: none;">
|
||||
<div id="left-widget-container">
|
||||
<div class="widget">
|
||||
<header>Puissances</header>
|
||||
<main>
|
||||
<div class="big-value">
|
||||
<span class="name">Courante</span>
|
||||
<span class="int-value" id="live-data-papp"></span><span style="visibility: hidden;">.<span class="dec-value"> </span></span>
|
||||
<span class="unit">VA</span>
|
||||
</div>
|
||||
<div class="big-value">
|
||||
<span class="name">Coupure</span>
|
||||
<span class="int-value" id="live-data-pcoup"></span><span style="visibility: hidden;">.<span class="dec-value"> </span></span>
|
||||
<span class="unit">VA</span>
|
||||
</div>
|
||||
<div class="big-value">
|
||||
<span class="name">Référence (souscrite)</span>
|
||||
<span class="int-value" id="live-data-pref"></span><span style="visibility: hidden;">.<span class="dec-value"> </span></span>
|
||||
<span class="unit">VA</span>
|
||||
</div>
|
||||
<div class="big-value">
|
||||
<span class="name">Max aujourd’hui</span>
|
||||
<span class="meta" id="live-data-pj-time">--:--</span>
|
||||
<span class="int-value" id="live-data-pj"></span><span style="visibility: hidden;">.<span class="dec-value"> </span></span>
|
||||
<span class="unit">VA</span>
|
||||
</div>
|
||||
<div class="big-value">
|
||||
<span class="name">Max hier</span>
|
||||
<span class="meta" id="live-data-pj-1-time">--:--</span>
|
||||
<span class="int-value" id="live-data-pj-1"></span><span style="visibility: hidden;">.<span class="dec-value"> </span></span>
|
||||
<span class="unit">VA</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div class="widget">
|
||||
<header>Voltage</header>
|
||||
<main>
|
||||
<div class="big-value">
|
||||
<span class="name">U<sub>eff</sub></span>
|
||||
<span class="int-value" id="live-data-urms"></span><span style="visibility: hidden;">.<span class="dec-value"> </span></span>
|
||||
<span class="unit">V</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div class="widget">
|
||||
<header>Indexes cumulés</header>
|
||||
<main id="live-data-indexes">
|
||||
<div id="live-data-indexes-template" class="big-value live-data-index" style="display: none;">
|
||||
<span class="name"></span>
|
||||
<span class="int-value"></span>.<span class="dec-value"> </span>
|
||||
<span class="unit">kWh</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div class="widget">
|
||||
<header>Indexes jour</header>
|
||||
<main id="live-data-indexes-day">
|
||||
<div id="live-data-indexes-day-template" class="big-value" style="display: none;">
|
||||
<span class="name"></span>
|
||||
<span class="meta"></span>
|
||||
<span class="int-value"></span>.<span class="dec-value"></span>
|
||||
<span class="unit">kWh</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div class="widget">
|
||||
<header>Couts depuis minuit</header>
|
||||
<main id="live-data-price-day">
|
||||
<div id="live-data-price-day-template" class="big-value" style="display: none;">
|
||||
<span class="name"></span>
|
||||
<span class="meta"><span class="live-data-price-per-kwh"></span> € / kWh</span>
|
||||
<span class="int-value"></span>.<span class="dec-value"></span>
|
||||
<span class="unit">€</span>
|
||||
</div>
|
||||
<div class="big-value">
|
||||
<span class="name">Total</span>
|
||||
<span class="int-value" id="live-data-price-total-int"></span>.<span class="dec-value" id="live-data-price-total-dec"></span>
|
||||
<span class="unit">€</span>
|
||||
</div>
|
||||
<div class="big-value">
|
||||
<span class="name">Abonnement</span>
|
||||
<span class="meta"><span id="live-data-price-sub-per-month"></span> € / mois</span>
|
||||
<span class="int-value" id="live-data-price-sub-int"></span>.<span class="dec-value" id="live-data-price-sub-dec"></span>
|
||||
<span class="unit">€</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div class="widget">
|
||||
<header>Infos compteur</header>
|
||||
<main>
|
||||
Num série : <span class="raw-value" id="live-data-serial"></span><br>
|
||||
PRM/<span title="Référence Point De Livraison">PDL</span> : <span class="raw-value" id="live-data-prm"></span><br>
|
||||
Tarif : <span class="raw-value" id="live-data-OPTARIF"></span><br>
|
||||
<span class="raw-value" id="live-data-status"></span>
|
||||
</main>
|
||||
</div>
|
||||
<div class="widget">
|
||||
<header>Air</header>
|
||||
<main id="live-data-sensors">
|
||||
<div id="live-data-sensors-template" class="big-value" style="display: none;">
|
||||
<span class="name"></span>
|
||||
<span class="meta"><span class="live-data-sensor-hum"></span> % Hum.</span>
|
||||
<span class="int-value"></span>.<span class="dec-value"></span>
|
||||
<span class="unit">°C</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div class="widget">
|
||||
<header>Connexion</header>
|
||||
<main>
|
||||
<form method="get" action="/_logout" target="login_frame">
|
||||
<input type="submit" value="Se déconnecter"/>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div id="right-widget-container">
|
||||
<div class="widget">
|
||||
<header>Widget 1</header>
|
||||
<main>Du contenu</main>
|
||||
</div>
|
||||
<div class="widget">
|
||||
<header>Widget 2</header>
|
||||
<main>Encore<br>du<br>contenu</main>
|
||||
</div>
|
||||
<div class="widget">
|
||||
<header>Widget 3</header>
|
||||
<main>Encore<br>du contenu</main>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div id="login-form" style="display: none;">
|
||||
<div class="widget">
|
||||
<header>Connexion</header>
|
||||
<main>
|
||||
<form method="post" action="/_login" onsubmit="setHTML('live-login-status', 'Connexion...');" target="login_frame">
|
||||
<input type="text" name="username" placeholder="Pseudo"/>
|
||||
<input type="password" name="password" placeholder="Mot de passe"/>
|
||||
<input type="submit" value="Connexion"/>
|
||||
<span class="raw-value" id="live-login-status"></span>
|
||||
</form>
|
||||
<iframe id="login_frame" name="login_frame" style="display: none;"></iframe>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<script src="index.js"></script>
|
233
res/static/index.js
Normal file
233
res/static/index.js
Normal file
@ -0,0 +1,233 @@
|
||||
|
||||
let subscriptionMonth = 10.60;
|
||||
let indexPrices = [0.1423, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
|
||||
|
||||
|
||||
let currentData = {};
|
||||
|
||||
function setHTML(id, html) {
|
||||
document.getElementById(id).innerHTML = html;
|
||||
}
|
||||
|
||||
function leftPadStr(value, pad, count) {
|
||||
value = value + '';
|
||||
while (value.length < count) {
|
||||
value = pad + value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function onLiveDataReceived(data) {
|
||||
if (data.serialNumber != currentData.serialNumber) {
|
||||
setHTML("live-data-serial", data.serialNumber);
|
||||
}
|
||||
if (data.prm != currentData.prm) {
|
||||
setHTML("live-data-prm", data.prm);
|
||||
}
|
||||
if (data.subscribedOption != currentData.subscribedOption) {
|
||||
setHTML("live-data-OPTARIF", data.subscribedOption);
|
||||
}
|
||||
if (data.refPower != currentData.refPower) {
|
||||
setHTML("live-data-pref", data.refPower);
|
||||
}
|
||||
if (data.cutPower != currentData.cutPower) {
|
||||
setHTML("live-data-pcoup", data.cutPower);
|
||||
}
|
||||
if (data.maxPowerToday != currentData.maxPowerToday) {
|
||||
setHTML("live-data-pj", data.maxPowerToday);
|
||||
var d = new Date(data.maxPowerTimeToday);
|
||||
setHTML("live-data-pj-time", d.toLocaleTimeString());
|
||||
}
|
||||
if (data.maxPowerYesterday != currentData.maxPowerYesterday) {
|
||||
setHTML("live-data-pj-1", data.maxPowerYesterday);
|
||||
var d = new Date(data.maxPowerTimeYesterday);
|
||||
setHTML("live-data-pj-1-time", d.toLocaleTimeString());
|
||||
}
|
||||
if (data.appPower != currentData.appPower) {
|
||||
setHTML("live-data-papp", data.appPower);
|
||||
}
|
||||
if (data.rmsVoltage != currentData.rmsVoltage) {
|
||||
setHTML("live-data-urms", data.rmsVoltage);
|
||||
}
|
||||
|
||||
if (data.indexes != currentData.indexes) {
|
||||
var parent = document.getElementById('live-data-indexes');
|
||||
var template = document.getElementById('live-data-indexes-template');
|
||||
for (var i = 0; i < data.indexes.length; i++) {
|
||||
var value = data.indexes[i];
|
||||
var name = data.indexNames[i] || ('Index ' + (i + 1));
|
||||
if (value <= 0)
|
||||
continue;
|
||||
var el = document.getElementById('live-data-index-' + i);
|
||||
if (el == null) {
|
||||
el = template.cloneNode(true);
|
||||
el.id = 'live-data-index-' + i;
|
||||
el.style.display = "";
|
||||
if (data.currIndex == i)
|
||||
el.classList.add('index-current');
|
||||
el = parent.appendChild(el);
|
||||
var nameEl = el.querySelector('.name');
|
||||
nameEl.innerHTML = name;
|
||||
}
|
||||
var kWhEl = el.querySelector('.int-value');
|
||||
var whEl = el.querySelector('.dec-value');
|
||||
kWhEl.innerHTML = Math.floor(value / 1000);
|
||||
whEl.innerHTML = leftPadStr(value % 1000, '0', 3);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.currIndex != currentData.currIndex) {
|
||||
var oldEl = document.getElementById('live-data-index-' + currentData.currIndex);
|
||||
if (oldEl != null) {
|
||||
oldEl.classList.remove('index-current');
|
||||
}
|
||||
var newEl = document.getElementById('live-data-index-' + data.currIndex);
|
||||
if (newEl != null) {
|
||||
newEl.classList.add('index-current');
|
||||
}
|
||||
}
|
||||
|
||||
var dayPriceSum = 0.0;
|
||||
|
||||
if (data.indexesMidnight != currentData.indexesMidnight) {
|
||||
var parentIndex = document.getElementById('live-data-indexes-day');
|
||||
var templateIndex = document.getElementById('live-data-indexes-day-template');
|
||||
var parentPrice = document.getElementById('live-data-price-day');
|
||||
var templatePrice = document.getElementById('live-data-price-day-template');
|
||||
for (var i = 0; i < data.indexesMidnight.length; i++) {
|
||||
var value = data.indexes[i] - data.indexesMidnight[i];
|
||||
var name = data.indexNames[i] || ('Index ' + (i + 1));
|
||||
if (value <= 0)
|
||||
continue;
|
||||
var el = document.getElementById('live-data-index-day-' + i);
|
||||
if (el == null) {
|
||||
el = templateIndex.cloneNode(true);
|
||||
el.id = 'live-data-index-day-' + i;
|
||||
el.style.display = "";
|
||||
el = parentIndex.appendChild(el);
|
||||
var nameEl = el.querySelector('.name');
|
||||
nameEl.innerHTML = name;
|
||||
}
|
||||
var kWhEl = el.querySelector('.int-value');
|
||||
var whEl = el.querySelector('.dec-value');
|
||||
kWhEl.innerHTML = Math.floor(value / 1000);
|
||||
whEl.innerHTML = leftPadStr(value % 1000, '0', 3);
|
||||
|
||||
el = document.getElementById('live-data-price-day-' + i);
|
||||
if (el == null) {
|
||||
el = templatePrice.cloneNode(true);
|
||||
el.id = 'live-data-price-day-' + i;
|
||||
el.style.display = "";
|
||||
el = parentPrice.appendChild(el);
|
||||
var nameEl = el.querySelector('.name');
|
||||
nameEl.innerHTML = name;
|
||||
}
|
||||
var price = value * (indexPrices[i] / 1000);
|
||||
dayPriceSum += price;
|
||||
var euroEl = el.querySelector('.int-value');
|
||||
var centsEl = el.querySelector('.dec-value');
|
||||
var perkWh = el.querySelector('.live-data-price-per-kwh');
|
||||
euroEl.innerHTML = Math.floor(price);
|
||||
centsEl.innerHTML = leftPadStr(Math.floor(price * 10000) % 10000, '0', 4);
|
||||
perkWh.innerHTML = indexPrices[i];
|
||||
}
|
||||
}
|
||||
|
||||
var dayPrice = subscriptionMonth / data.nbDayThisMonth;
|
||||
var currDayPrice = dayPrice * ((data.date - data.getDayStartTime) / 86400000);
|
||||
dayPriceSum += currDayPrice;
|
||||
setHTML('live-data-price-sub-int', Math.floor(currDayPrice));
|
||||
setHTML('live-data-price-sub-dec', leftPadStr(Math.floor(currDayPrice * 10000) % 10000, '0', 4));
|
||||
|
||||
setHTML('live-data-price-total-int', Math.floor(dayPriceSum));
|
||||
setHTML('live-data-price-total-dec', leftPadStr(Math.floor(dayPriceSum * 10000) % 10000, '0', 4));
|
||||
|
||||
setHTML('live-data-price-sub-per-month', subscriptionMonth);
|
||||
|
||||
|
||||
|
||||
if (data.sensorsData != currentData.sensorsData) {
|
||||
var parent = document.getElementById('live-data-sensors');
|
||||
var template = document.getElementById('live-data-sensors-template');
|
||||
for (var name in data.sensorsData) {
|
||||
var temp = data.sensorsData[name].temp;
|
||||
var hum = data.sensorsData[name].hum;
|
||||
var el = document.getElementById('live-data-sensors-' + name);
|
||||
if (el == null) {
|
||||
el = template.cloneNode(true);
|
||||
el.id = 'live-data-sensors-' + name;
|
||||
el.style.display = "";
|
||||
el = parent.appendChild(el);
|
||||
var nameEl = el.querySelector('.name');
|
||||
nameEl.innerHTML = name;
|
||||
}
|
||||
var intEl = el.querySelector('.int-value');
|
||||
var decEl = el.querySelector('.dec-value');
|
||||
var humEl = el.querySelector('.live-data-sensor-hum');
|
||||
intEl.innerHTML = Math.floor(temp);
|
||||
decEl.innerHTML = leftPadStr(Math.floor(temp * 10) % 10, '0', 1);
|
||||
humEl.innerHTML = Math.floor(hum);
|
||||
}
|
||||
}
|
||||
|
||||
currentData = data;
|
||||
}
|
||||
|
||||
function loginSuccess() {
|
||||
document.getElementById("logged-in-content").style.display = "";
|
||||
document.getElementById("login-form").style.display = "none";
|
||||
setHTML("live-login-status", "");
|
||||
}
|
||||
|
||||
function loginFail(message) {
|
||||
document.getElementById("logged-in-content").style.display = "none";
|
||||
document.getElementById("login-form").style.display = "";
|
||||
setHTML("live-login-status", message);
|
||||
}
|
||||
|
||||
function updateLiveData(afterLogin = false) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onload = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 403) {
|
||||
loginFail(afterLogin ? "Mauvais identifiants" : "");
|
||||
return;
|
||||
}
|
||||
loginSuccess();
|
||||
if (xhr.status == 200) {
|
||||
setHTML("live-data-status", "");
|
||||
var jsonObj = JSON.parse(xhr.responseText);
|
||||
onLiveDataReceived(jsonObj.data);
|
||||
var delay = jsonObj.avgUpdateInterval - (jsonObj.now - jsonObj.lastUpdate) + 100;
|
||||
if (delay < 200) // dont spam if data source is too late than usual
|
||||
delay = 200;
|
||||
setTimeout(function() { updateLiveData(false); }, delay);
|
||||
}
|
||||
else {
|
||||
setHTML("live-data-status", "Erreur de connexion (backend offline)");
|
||||
setTimeout(function() { updateLiveData(false); }, 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.ontimeout = function() {
|
||||
setHTML("live-data-status", "Erreur de connexion (timeout)");
|
||||
setTimeout(function() { updateLiveData(false); }, 5000);
|
||||
}
|
||||
xhr.onerror = function() {
|
||||
setHTML("live-data-status", "Erreur de connexion");
|
||||
setTimeout(function() { updateLiveData(false); }, 5000);
|
||||
}
|
||||
xhr.timeout = 5000;
|
||||
xhr.open("GET", "/rest/currentData", true);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
|
||||
|
||||
document.getElementById("login_frame").onload = function() {
|
||||
updateLiveData(true);
|
||||
}
|
||||
|
||||
|
||||
updateLiveData(false);
|
11
run.sh
Normal file
11
run.sh
Normal file
@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
# The run.sh file must be the entry point of the docker container. It is not meant to be run by a user it a terminal.
|
||||
# Please use update.sh instead
|
||||
|
||||
ls -la $TIC_TTY_INPUT
|
||||
echo "Initializing serial port"
|
||||
stty -F $TIC_TTY_INPUT 9600 raw cs7 parenb
|
||||
|
||||
echo "Starting TIC server"
|
||||
java -cp res -jar /data/bin/tic.jar
|
27
src/main/java/fr/mbaloup/home/Main.java
Normal file
27
src/main/java/fr/mbaloup/home/Main.java
Normal file
@ -0,0 +1,27 @@
|
||||
package fr.mbaloup.home;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.rapidoid.log.Log;
|
||||
|
||||
import fr.mbaloup.home.tic.TICDataDispatcher;
|
||||
import fr.mbaloup.home.tic.TICRawDecoder;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static TICDataDispatcher tic;
|
||||
public static void main(String[] args) throws IOException, InterruptedException {
|
||||
|
||||
Log.info("Initializing TIC raw decoder...");
|
||||
TICRawDecoder decoder = new TICRawDecoder();
|
||||
|
||||
Log.info("Initializing TIC data dispatcher...");
|
||||
tic = new TICDataDispatcher(decoder);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
118
src/main/java/fr/mbaloup/home/mqtt/MQTTSender.java
Normal file
118
src/main/java/fr/mbaloup/home/mqtt/MQTTSender.java
Normal file
@ -0,0 +1,118 @@
|
||||
package fr.mbaloup.home.mqtt;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.eclipse.paho.client.mqttv3.IMqttClient;
|
||||
import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
||||
import org.eclipse.paho.client.mqttv3.MqttException;
|
||||
import org.rapidoid.log.Log;
|
||||
|
||||
public class MQTTSender {
|
||||
|
||||
|
||||
private static final String SERVER_URI = "tcp://" + System.getenv("TIC_MQTT_SERVER_IP");
|
||||
private static final String HASS_DISCOVERY_PREFIX = System.getenv("TIC_MQTT_HASS_DISCOVERY_PREFIX");
|
||||
private static final String USER = System.getenv("TIC_MQTT_USERNAME");
|
||||
private static final String PASSWORD = System.getenv("TIC_MQTT_PASSWORD");
|
||||
|
||||
public static final Topic<Integer> INDEX_TOTAL = new Topic<>("tic/index/_total_", 1, true, "tic_index_total", "TIC Index Total", "Wh", "energy", "total_increasing");
|
||||
public static final Topic<Integer> APPARENT_POWER = new Topic<>("tic/power/app", 0, true, "tic_power_app", "TIC Apparent power", "VA", "energy", "measurement");
|
||||
public static final Topic<Integer> CUT_POWER = new Topic<>("tic/power/cut", 0, true, "tic_power_cut", "TIC Cut power", "VA", "energy", null);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private static final String clientId = UUID.randomUUID().toString();
|
||||
|
||||
private static IMqttClient publisher = null;
|
||||
|
||||
private static void connect() {
|
||||
try {
|
||||
publisher = new MqttClient(SERVER_URI, clientId);
|
||||
|
||||
MqttConnectOptions options = new MqttConnectOptions();
|
||||
options.setAutomaticReconnect(true);
|
||||
options.setCleanSession(true);
|
||||
options.setConnectionTimeout(60);
|
||||
options.setUserName(USER);
|
||||
options.setPassword(PASSWORD.toCharArray());
|
||||
publisher.connect(options);
|
||||
} catch (MqttException e) {
|
||||
Log.error("Cannot connect to MQTT broker.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized void publish(String topic, String value, int qos, boolean retained) {
|
||||
if (publisher == null)
|
||||
connect();
|
||||
try {
|
||||
publisher.publish(topic, value.getBytes(StandardCharsets.UTF_8), qos, retained);
|
||||
} catch (MqttException e) {
|
||||
Log.error("Cannot publish MQTT message.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static class Topic<T> {
|
||||
private final String topic;
|
||||
private final int qos;
|
||||
private final boolean retained;
|
||||
|
||||
private final String hassSensorId;
|
||||
private final String hassName;
|
||||
private final String hassUnit;
|
||||
private final String hassDeviceClass;
|
||||
private final String hassStateClass;
|
||||
|
||||
private boolean hassConfigured = false;
|
||||
|
||||
protected Topic(String t, int q, boolean r, String id, String name, String unit, String dClass, String sClass) {
|
||||
topic = t;
|
||||
qos = q;
|
||||
retained = r;
|
||||
hassSensorId = id;
|
||||
hassName = name;
|
||||
hassUnit = unit;
|
||||
hassDeviceClass = dClass;
|
||||
hassStateClass = sClass;
|
||||
}
|
||||
|
||||
public synchronized void publish(T value) {
|
||||
if (!hassConfigured)
|
||||
configure();
|
||||
MQTTSender.publish(topic, valueToString(value), qos, retained);
|
||||
}
|
||||
|
||||
private void configure() {
|
||||
|
||||
String hassJson = "{"
|
||||
+ "\"name\":\"" + hassName + "\","
|
||||
+ "\"unique_id\":\"" + hassSensorId + "\","
|
||||
+ "\"state_topic\":\"" + topic + "\"";
|
||||
if (hassUnit != null)
|
||||
hassJson += ",\"unit_of_measurement\":\"" + hassUnit + "\"";
|
||||
if (hassDeviceClass != null)
|
||||
hassJson += ",\"device_class\":\"" + hassDeviceClass + "\"";
|
||||
if (hassStateClass != null)
|
||||
hassJson += ",\"state_class\":\"" + hassStateClass + "\"";
|
||||
hassJson += "}";
|
||||
|
||||
MQTTSender.publish(HASS_DISCOVERY_PREFIX + "/sensor/" + hassSensorId + "/config", hassJson, 1, true);
|
||||
hassConfigured = true;
|
||||
}
|
||||
|
||||
protected String valueToString(T value) {
|
||||
return Objects.toString(value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
31
src/main/java/fr/mbaloup/home/tic/DataConverter.java
Normal file
31
src/main/java/fr/mbaloup/home/tic/DataConverter.java
Normal file
@ -0,0 +1,31 @@
|
||||
package fr.mbaloup.home.tic;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.Objects;
|
||||
|
||||
public class DataConverter {
|
||||
|
||||
// see https://www.enedis.fr/sites/default/files/Enedis-NOI-CPT_54E.pdf §6.2.1.1
|
||||
public static long fromDateToMillis(String date) {
|
||||
Objects.requireNonNull(date, "date cannot be null");
|
||||
if (date.length() != 13)
|
||||
throw new IllegalArgumentException("date must have exactly 13 characters");
|
||||
|
||||
int i = 0;
|
||||
char season = date.charAt(i++);
|
||||
String offsetStr = Character.toLowerCase(season) == 'e' ? "+02:00" : "+01:00";
|
||||
String yearStr = date.substring(i, i += 2);
|
||||
String monthStr = date.substring(i, i += 2);
|
||||
String dayStr = date.substring(i, i += 2);
|
||||
String hourStr = date.substring(i, i += 2);
|
||||
String minStr = date.substring(i, i += 2);
|
||||
String secStr = date.substring(i, i + 2);
|
||||
|
||||
TemporalAccessor ta = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse("20"+yearStr+"-"+monthStr+"-"+dayStr+"T"+hourStr+":"+minStr+":"+secStr+offsetStr);
|
||||
return Instant.from(ta).toEpochMilli();
|
||||
}
|
||||
|
||||
|
||||
}
|
25
src/main/java/fr/mbaloup/home/tic/DataFrame.java
Normal file
25
src/main/java/fr/mbaloup/home/tic/DataFrame.java
Normal file
@ -0,0 +1,25 @@
|
||||
package fr.mbaloup.home.tic;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
public class DataFrame {
|
||||
public final long time;
|
||||
public final Map<String, DataSet> data;
|
||||
public DataFrame(long t, Map<String, DataSet> ig) {
|
||||
time = t;
|
||||
data = Collections.unmodifiableMap(ig);
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{time=" + time + ",infoGroups=" + data + "}";
|
||||
}
|
||||
public void ifKeyPresent(String k, DataSetExecutor run) {
|
||||
if (data.containsKey(k))
|
||||
run.consume(k, data.get(k), time);
|
||||
}
|
||||
|
||||
public interface DataSetExecutor {
|
||||
void consume(String k, DataSet ds, long dfTime);
|
||||
}
|
||||
}
|
9
src/main/java/fr/mbaloup/home/tic/DataSet.java
Normal file
9
src/main/java/fr/mbaloup/home/tic/DataSet.java
Normal file
@ -0,0 +1,9 @@
|
||||
package fr.mbaloup.home.tic;
|
||||
|
||||
public class DataSet {
|
||||
public final Long time;
|
||||
public final String data;
|
||||
public DataSet(Long t, String d) {
|
||||
time = t; data = d;
|
||||
}
|
||||
}
|
109
src/main/java/fr/mbaloup/home/tic/SimulatedTICInputStream.java
Normal file
109
src/main/java/fr/mbaloup/home/tic/SimulatedTICInputStream.java
Normal file
@ -0,0 +1,109 @@
|
||||
package fr.mbaloup.home.tic;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Random;
|
||||
|
||||
public class SimulatedTICInputStream extends PipedInputStream {
|
||||
|
||||
PipedOutputStream out = new PipedOutputStream();
|
||||
|
||||
Thread th;
|
||||
|
||||
public SimulatedTICInputStream() throws IOException {
|
||||
super(2048);
|
||||
connect(out);
|
||||
th = new Thread(this::run, "Simulated TIC Thread");
|
||||
th.setDaemon(true);
|
||||
th.start();
|
||||
}
|
||||
|
||||
private boolean closed = false;
|
||||
|
||||
private int indexHC = 0;
|
||||
private int indexHP = 0;
|
||||
private long lastTime = System.currentTimeMillis();
|
||||
private long wattSecondsCumul = 0; // 3600 Ws = 1 Wh
|
||||
|
||||
private final Random rand = new Random();
|
||||
|
||||
private int count = 0;
|
||||
|
||||
private void run() {
|
||||
while (!closed) {
|
||||
|
||||
Calendar cal = new GregorianCalendar();
|
||||
boolean hc = (cal.get(Calendar.HOUR_OF_DAY) < 8);
|
||||
int power = cal.get(Calendar.MINUTE) % 2 == 0 ? 2500 : 200;
|
||||
float r = rand.nextFloat();
|
||||
power += r < 0.2 ? 10 : r < 0.4 ? -10 : 0;
|
||||
|
||||
long newT = cal.getTimeInMillis();
|
||||
wattSecondsCumul += power * (newT - lastTime) / 1000;
|
||||
if (wattSecondsCumul > 3600) {
|
||||
int indexIncr = (int) (wattSecondsCumul / 3600);
|
||||
wattSecondsCumul -= indexIncr * 3600L;
|
||||
if (hc)
|
||||
indexHC += indexIncr;
|
||||
else
|
||||
indexHP += indexIncr;
|
||||
}
|
||||
lastTime = newT;
|
||||
|
||||
write(TICRawDecoder.FRAME_START);
|
||||
writeInfoGroup("ADCO", "DUMMY");
|
||||
writeInfoGroup("OPTARIF", "HC..");
|
||||
writeInfoGroup("ISOUSC", "30");
|
||||
writeInfoGroup("HCHC", String.format("%08d", indexHC));
|
||||
writeInfoGroup("HCHP", String.format("%08d", indexHP));
|
||||
writeInfoGroup("PTEC", hc ? "HC.." : "HP..");
|
||||
writeInfoGroup("IINST", String.format("%03d", Math.round(power / (float) 200)));
|
||||
writeInfoGroup("IMAX", "090");
|
||||
writeInfoGroup("PAPP", String.format("%05d", power));
|
||||
writeInfoGroup("HHPHC", "A");
|
||||
writeInfoGroup("MOTDETAT", String.format("%06d", (count++) % 1000000));
|
||||
write(TICRawDecoder.FRAME_END);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeInfoGroup(String key, String value) {
|
||||
try {
|
||||
Thread.sleep(8);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
write(TICRawDecoder.DATASET_START);
|
||||
write(key);
|
||||
write(TICRawDecoder.SEP_SP);
|
||||
write(value);
|
||||
write(TICRawDecoder.SEP_SP);
|
||||
write(TICRawDecoder.checksum(key + TICRawDecoder.SEP_SP + value + TICRawDecoder.SEP_SP));
|
||||
write(TICRawDecoder.DATASET_END);
|
||||
}
|
||||
|
||||
private void write(String s) {
|
||||
for (char c : s.toCharArray())
|
||||
write(c);
|
||||
}
|
||||
|
||||
private synchronized void write(char c) {
|
||||
if (closed)
|
||||
return;
|
||||
try {
|
||||
Thread.sleep(2);
|
||||
out.write(c);
|
||||
} catch (InterruptedException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
closed = true;
|
||||
super.close();
|
||||
}
|
||||
|
||||
}
|
931
src/main/java/fr/mbaloup/home/tic/TICDataDispatcher.java
Normal file
931
src/main/java/fr/mbaloup/home/tic/TICDataDispatcher.java
Normal file
@ -0,0 +1,931 @@
|
||||
package fr.mbaloup.home.tic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.rapidoid.log.Log;
|
||||
|
||||
import fr.mbaloup.home.mqtt.MQTTSender;
|
||||
import fr.mbaloup.home.tic.DataFrame.DataSetExecutor;
|
||||
import fr.mbaloup.home.tic.TICDataDispatcher.DayScheduleTimeSlot.PhysicalRelayChange;
|
||||
|
||||
public class TICDataDispatcher extends Thread {
|
||||
|
||||
|
||||
|
||||
private final TICRawDecoder decoder;
|
||||
|
||||
public TICDataDispatcher(TICRawDecoder ticRaw) {
|
||||
super("TIC Dispatcher Thread");
|
||||
decoder = ticRaw;
|
||||
start();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
for (;;) {
|
||||
try {
|
||||
onDataUpdate(decoder.getNextDataFrame());
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
Log.error("Error while handling a data frame", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private synchronized void onDataUpdate(DataFrame df) {
|
||||
|
||||
if (df.data.containsKey("ADCO"))
|
||||
updateHistorical(df);
|
||||
else
|
||||
updateStandard(df);
|
||||
|
||||
|
||||
|
||||
|
||||
lastUpdates.addLast(System.currentTimeMillis());
|
||||
while (lastUpdates.size() > 32)
|
||||
lastUpdates.removeFirst();
|
||||
|
||||
if (lastUpdates.size() >= 2)
|
||||
avgUpdateInterval = (lastUpdates.getLast() - lastUpdates.getFirst()) / (lastUpdates.size() - 1);
|
||||
}
|
||||
|
||||
|
||||
private void updateHistorical(DataFrame df) {
|
||||
systTime = df.time;
|
||||
df.ifKeyPresent("ADCO", (k, ds, t) -> {
|
||||
updateSerialNumber(ds.data);
|
||||
updateTICVersion(null);
|
||||
});
|
||||
df.ifKeyPresent("OPTARIF", (k, ds, t) -> updateSubscribedOption(k, ds.data));
|
||||
df.ifKeyPresent("ISOUSC", (k, ds, t) -> updateHistoricalSubscribedIntensity(ds.data));
|
||||
|
||||
DataSetExecutor indexExecutor = (k, ds, t) -> updateIndexHist(k, ds.data);
|
||||
df.ifKeyPresent("BASE", indexExecutor);
|
||||
df.ifKeyPresent("HCHC", indexExecutor);
|
||||
df.ifKeyPresent("HCHP", indexExecutor);
|
||||
df.ifKeyPresent("EJPHN", indexExecutor);
|
||||
df.ifKeyPresent("EJPHPM", indexExecutor);
|
||||
df.ifKeyPresent("BBRHCJB", indexExecutor);
|
||||
df.ifKeyPresent("BBRHPJB", indexExecutor);
|
||||
df.ifKeyPresent("BBRHCJW", indexExecutor);
|
||||
df.ifKeyPresent("BBRHPJW", indexExecutor);
|
||||
df.ifKeyPresent("BBRHCJR", indexExecutor);
|
||||
df.ifKeyPresent("BBRHPJR", indexExecutor);
|
||||
|
||||
histMobilePointNotice = df.data.containsKey("PEJP");
|
||||
df.ifKeyPresent("PTEC", (k, ds, t) -> updateCurrIndexHist(ds.data));
|
||||
df.ifKeyPresent("DEMAIN", (k, ds, t) -> updateDEMAIN(ds.data));
|
||||
df.ifKeyPresent("IINST", (k, ds, t) -> updateRMSCurrent(ds.data));
|
||||
overPowerConsumption = df.data.containsKey("ADPS");
|
||||
df.ifKeyPresent("IMAX", (k, ds, t) -> {
|
||||
/* unclear what represent this data, except "Intensité maximale appelée".
|
||||
* On Linky electric meter with historical TIC mode,
|
||||
* the received value is always 90 A.
|
||||
*/
|
||||
});
|
||||
df.ifKeyPresent("PAPP", (k, ds, t) -> updateApparentPower(ds.data));
|
||||
df.ifKeyPresent("HHPHC", (k, ds, t) -> updateHHPHC(ds.data));
|
||||
df.ifKeyPresent("MOTDETAT", (k, ds, t) -> updateMOTDETAT(ds.data));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void updateStandard(DataFrame df) {
|
||||
df.ifKeyPresent("DATE", (k, ds, t) -> updateTICTimeStd(t, ds.time));
|
||||
df.ifKeyPresent("ADSC", (k, ds, t) -> updateSerialNumber(ds.data));
|
||||
df.ifKeyPresent("VTIC", (k, ds, t) -> updateTICVersion(ds.data));
|
||||
df.ifKeyPresent("NGTF", (k, ds, t) -> updateSubscribedOption(k, ds.data));
|
||||
df.ifKeyPresent("NTARF", (k, ds, t) -> {
|
||||
// NTARF must be updated before LTARF
|
||||
updateCurrentIndexId(ds.data);
|
||||
df.ifKeyPresent("LTARF", (k2, ds2, t2) -> updateNameOfCurrentIndex(ds2.data));
|
||||
});
|
||||
|
||||
df.ifKeyPresent("EAST", (k, ds, t) -> updateTotalIndexStd(ds.data));
|
||||
|
||||
DataSetExecutor indexExecutor = (k, ds, t) -> updateIndexStd(k.substring(4), ds.data);
|
||||
df.ifKeyPresent("EASF01", indexExecutor);
|
||||
df.ifKeyPresent("EASF02", indexExecutor);
|
||||
df.ifKeyPresent("EASF03", indexExecutor);
|
||||
df.ifKeyPresent("EASF04", indexExecutor);
|
||||
df.ifKeyPresent("EASF05", indexExecutor);
|
||||
df.ifKeyPresent("EASF06", indexExecutor);
|
||||
df.ifKeyPresent("EASF07", indexExecutor);
|
||||
df.ifKeyPresent("EASF08", indexExecutor);
|
||||
df.ifKeyPresent("EASF09", indexExecutor);
|
||||
df.ifKeyPresent("EASF10", indexExecutor);
|
||||
|
||||
DataSetExecutor distIndexExecutor = (k, ds, t) -> updateDistributorIndex(k.substring(4), ds.data);
|
||||
df.ifKeyPresent("EASD01", distIndexExecutor);
|
||||
df.ifKeyPresent("EASD02", distIndexExecutor);
|
||||
df.ifKeyPresent("EASD03", distIndexExecutor);
|
||||
df.ifKeyPresent("EASD04", distIndexExecutor);
|
||||
|
||||
df.ifKeyPresent("IRMS1", (k, ds, t) -> updateRMSCurrent(ds.data));
|
||||
df.ifKeyPresent("URMS1", (k, ds, t) -> updateRMSVoltage(ds.data));
|
||||
df.ifKeyPresent("PREF", (k, ds, t) -> updateRefPower(ds.data));
|
||||
df.ifKeyPresent("PCOUP", (k, ds, t) -> updateCutPower(ds.data));
|
||||
df.ifKeyPresent("SINSTS", (k, ds, t) -> updateApparentPower(ds.data));
|
||||
df.ifKeyPresent("SMAXSN", (k, ds, t) -> updateMaxPowerToday(ds.time, ds.data));
|
||||
df.ifKeyPresent("SMAXSN-1", (k, ds, t) -> updateMaxPowerYesterday(ds.time, ds.data));
|
||||
df.ifKeyPresent("UMOY1", (k, ds, t) -> updateAvgVoltage(ds.time, ds.data));
|
||||
df.ifKeyPresent("PRM", (k, ds, t) -> updatePRM(ds.data));
|
||||
df.ifKeyPresent("NJOURF", (k, ds, t) -> updateProviderCalDayToday(ds.data));
|
||||
df.ifKeyPresent("NJOURF+1", (k, ds, t) -> updateProviderCalDayTomorrow(ds.data));
|
||||
df.ifKeyPresent("MSG1", (k, ds, t) -> updateMessage1(ds.data));
|
||||
updateMessage2(df.data.containsKey("MSG2") ? df.data.get("MSG2").data : null);
|
||||
df.ifKeyPresent("STGE", (k, ds, t) -> updateStatusRegister(ds.data)); // 4 last bits ignored for the moment
|
||||
df.ifKeyPresent("RELAIS", (k, ds, t) -> updateRelaysStatus(ds.data));
|
||||
df.ifKeyPresent("PJOURF+1", (k, ds, t) -> updateNextDayScheduling(ds.data));
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private final LinkedList<Long> lastUpdates = new LinkedList<>();
|
||||
private long avgUpdateInterval = -1;
|
||||
/**
|
||||
* The average time interval between each consecutive data frame received from the TIC.
|
||||
* The average is measured using the 32 last data framesm
|
||||
* @return A time interval, in ms.
|
||||
*/
|
||||
public synchronized long getAverageUpdateInterval() {
|
||||
return avgUpdateInterval;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The time of when the last dataframe was fully received by the system, according to the system clock.
|
||||
* <p>
|
||||
* To have the time of the dataframe according to the electric meter, use {@link #getTICTime()}.
|
||||
* To hae the time of when the first byte of the dataframe was received, use {@link #getFrameSystemTime()}.
|
||||
* @return the millisecond epoch time of when the last dataframe was fully received by the system.
|
||||
*/
|
||||
public synchronized long getLastUpdateTime() {
|
||||
return lastUpdates.isEmpty() ? -1 : lastUpdates.getLast();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private Long systTime = null;
|
||||
private Long ticTime = null;
|
||||
private Long rawTICTime = null;
|
||||
private void updateTICTimeStd(long newSystTime, long newTICTime) {
|
||||
rawTICTime = newTICTime;
|
||||
|
||||
/* Two consecutive data frame may have the same DATE value if the interval between 2 dataframe is below 1 second.
|
||||
* In this case, this is necessary to determine a time closer to the real time (in the counter) to avoid bad
|
||||
* interpretation of the data in subsequent processing (storage, display, ...).
|
||||
*/
|
||||
long ticTimeMin = newTICTime;
|
||||
long ticTimeMax = newTICTime+999;
|
||||
|
||||
if (systTime == null) { // no previous data
|
||||
if (newSystTime > ticTimeMin && newSystTime <= ticTimeMax) {
|
||||
newTICTime = newSystTime;
|
||||
}
|
||||
else if (newSystTime > ticTimeMax) {
|
||||
newTICTime = ticTimeMax;
|
||||
}
|
||||
}
|
||||
else {
|
||||
long interval = newSystTime - systTime;
|
||||
long estimatedNewPreciseTICTime = ticTime + interval;
|
||||
if (estimatedNewPreciseTICTime > ticTimeMin && estimatedNewPreciseTICTime <= ticTimeMax) {
|
||||
newTICTime = estimatedNewPreciseTICTime;
|
||||
}
|
||||
else if (estimatedNewPreciseTICTime > ticTimeMax) {
|
||||
newTICTime = ticTimeMax;
|
||||
}
|
||||
}
|
||||
|
||||
systTime = newSystTime;
|
||||
ticTime = newTICTime;
|
||||
}
|
||||
/**
|
||||
* The time of when the first byte of the last dataframe was received, according to the system clock.
|
||||
* <p>
|
||||
* To have the time of the dataframe according to the electric meter, use {@link #getTICTime()}.
|
||||
* @return the millisecond epoch time of when the first byte of the last dataframe was received.
|
||||
*/
|
||||
public synchronized Long getFrameSystemTime() { return systTime; }
|
||||
/**
|
||||
* The time of the dataframe, according to the electric meter.
|
||||
* <p>
|
||||
* The time is received from the meter with a precision of a second. The value with millisecond precision
|
||||
< |