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
|
||||
* is estimated using the time interval between the current and the last dataframe, and using
|
||||
* the system clock.
|
||||
* If you want to have the time, as declared by the TIC dataframe, use {@link #getRawTICTime()}.
|
||||
* <p>
|
||||
* The TIC time may have shifted from the real time. In this case, {@link #isInternalClockBad()} will return true.
|
||||
* <p>
|
||||
* To have the time of the dataframe according to the system clock, use {@link #getFrameSystemTime()}.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided.</li>
|
||||
* <li>Standard mode: DATE field.</li>
|
||||
* </ul>
|
||||
* @return the millisecond epoch time of the last dataset according to the electric meter.
|
||||
*/
|
||||
public synchronized Long getTICTime() { return ticTime; }
|
||||
/**
|
||||
* The time of the dataframe, according to the electric meter.
|
||||
* <p>
|
||||
* The time is received from the meter with a precision of a second.
|
||||
* If you want to have the time with millisecond precision, estimated using the system time, use {@link #getTICTime()}.
|
||||
* <p>
|
||||
* The TIC time may have shifted from the real time. In this case, {@link #isInternalClockBad()} will return true.
|
||||
* <p>
|
||||
* To have the time of the dataframe according to the system clock, use {@link #getFrameSystemTime()}.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided.</li>
|
||||
* <li>Standard mode: DATE field.</li>
|
||||
* </ul>
|
||||
* @return the millisecond epoch time of the last dataset according to the electric meter.
|
||||
*/
|
||||
public synchronized Long getRawTICTime() { return rawTICTime; }
|
||||
|
||||
|
||||
|
||||
private String TICVersion = null;
|
||||
private void updateTICVersion(String v) { TICVersion = v; }
|
||||
/**
|
||||
* Specification version of the TIC protocol.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided.</li>
|
||||
* <li>Standard mode: VTIC field.</li>
|
||||
* </ul>
|
||||
* This current data dispatcher supports the protocol {@code 02}.
|
||||
*
|
||||
* @return the specification version of the TIC protocol, or {@code null} if the TIC is in historical mode.
|
||||
*/
|
||||
public synchronized String getTICVersion() { return TICVersion; }
|
||||
|
||||
|
||||
|
||||
private String serialNumber = null;
|
||||
private void updateSerialNumber(String a) { serialNumber = a; }
|
||||
/**
|
||||
* Serial number of the electric meter, as written physically on the front cover (except the 2 last digits).
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: ADCO field.</li>
|
||||
* <li>Standard mode: ADCS field.</li>
|
||||
* </ul>
|
||||
* @return the serial number of the electric meter.
|
||||
*/
|
||||
public synchronized String getSerialNumber() { return serialNumber; }
|
||||
public synchronized String getSerialNumberManufacturerCode() {
|
||||
return serialNumber == null ? null : serialNumber.substring(0, 2);
|
||||
}
|
||||
public synchronized Integer getSerialNumberManufactureYear() {
|
||||
return serialNumber == null ? null : (Integer.parseInt(serialNumber.substring(2, 4)) + 2000);
|
||||
}
|
||||
public synchronized String getSerialNumberMeterType() {
|
||||
return serialNumber == null ? null : serialNumber.substring(4, 6);
|
||||
}
|
||||
public synchronized String getSerialNumberUniquePart() {
|
||||
return serialNumber == null ? null : serialNumber.substring(6);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private String prm = null;
|
||||
private void updatePRM(String a) { prm = a; }
|
||||
/**
|
||||
* Serial number of the electric meter, as written physically on the front cover (except the 2 last digits).
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided.</li>
|
||||
* <li>Standard mode: PRM field.</li>
|
||||
* </ul>
|
||||
* @return the serial number of the electric meter.
|
||||
*/
|
||||
public synchronized String getPRM() { return prm; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private String subscribedOption = null;
|
||||
private void updateSubscribedOption(String k, String o) {
|
||||
if (k.equals("OPTARIF") && o.equals("HC.."))
|
||||
subscribedOption = "H PLEINE/CREUSE";
|
||||
else if (k.equals("OPTARIF") && o.equals("EJP."))
|
||||
subscribedOption = "EJP";
|
||||
else
|
||||
subscribedOption = o.trim();
|
||||
}
|
||||
/**
|
||||
* The name or the id of the currently active subscription option.
|
||||
* <p>
|
||||
* Some example of price schedule option are constant price, peak/off-peak time prices, ...
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: OPTARIF field, with standardized values: {@code BASE}, {@code HC} or {@code EJP}.</li>
|
||||
* <li>Standard mode: NGTF field, the content being a non-standardized display name.</li>
|
||||
* </ul>
|
||||
* @return the price schedule option subscribed by the consumer.
|
||||
*/
|
||||
public synchronized String getSubscribedOption() { return subscribedOption; }
|
||||
|
||||
|
||||
|
||||
private Integer subscribedIntensity = null;
|
||||
private void updateHistoricalSubscribedIntensity(String i) {
|
||||
subscribedIntensity = Integer.parseInt(i);
|
||||
|
||||
Integer oldCutPower = cutPower;
|
||||
referencePower = cutPower = subscribedIntensity * 200;
|
||||
|
||||
if (!Objects.equals(cutPower, oldCutPower))
|
||||
MQTTSender.CUT_POWER.publish(cutPower);
|
||||
}
|
||||
/**
|
||||
* The subscribed intensity.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: ISOUSC field.</li>
|
||||
* <li>Standard mode: not provided directly, but we use {@code getReferencePower() / 200} to set this value.</li>
|
||||
* </ul>
|
||||
* @return the subscribed intensity, in Ampere.
|
||||
* @deprecated the consumption limit is provided in the contract in term of apparent power (kVA), not intensity (A).
|
||||
* Also, the transmitted intensity (in both historical and standard TIC mode) is not accurate since
|
||||
* it is based on the assumption that the voltage is always 200 V.
|
||||
*/
|
||||
@Deprecated
|
||||
public synchronized Integer getSubscribedIntensity() { return subscribedIntensity; }
|
||||
|
||||
|
||||
|
||||
private Integer referencePower = null;
|
||||
private void updateRefPower(String pRef) {
|
||||
referencePower = Integer.parseInt(pRef) * 1000;
|
||||
subscribedIntensity = referencePower / 200;
|
||||
}
|
||||
/**
|
||||
* The subscribed maximum apparent power.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided directly, but we use {@link #getSubscribedIntensity()} {@code * 200} to set this value.</li>
|
||||
* <li>Standard mode: PREF field in kVA.</li>
|
||||
* </ul>
|
||||
* @return the reference (subscribed) apparent power, in Volt Ampere (VA).
|
||||
*/
|
||||
public synchronized Integer getReferencePower() { return referencePower; }
|
||||
|
||||
|
||||
|
||||
private Integer cutPower = null;
|
||||
private void updateCutPower(String p) {
|
||||
Integer oldCutPower = cutPower;
|
||||
|
||||
cutPower = Integer.parseInt(p) * 1000;
|
||||
|
||||
if (!Objects.equals(cutPower, oldCutPower))
|
||||
MQTTSender.CUT_POWER.publish(cutPower);
|
||||
}
|
||||
/**
|
||||
* The maximum apparent power allowed before cutting the current.
|
||||
* <p>
|
||||
* May be different than the reference power.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided directly, but we use {@link #getSubscribedIntensity()} {@code * 200} to set this value.</li>
|
||||
* <li>Standard mode: PCOUP field in kVA.</li>
|
||||
* </ul>
|
||||
* @return the maximum allowed apparent power, in Volt Ampere (VA).
|
||||
*/
|
||||
public synchronized Integer getCutPower() { return cutPower; }
|
||||
|
||||
|
||||
private Integer apparentPower = null;
|
||||
private void updateApparentPower(String i) {
|
||||
Integer oldApparentPower = apparentPower;
|
||||
|
||||
apparentPower = Integer.parseInt(i);
|
||||
|
||||
if (!Objects.equals(apparentPower, oldApparentPower))
|
||||
MQTTSender.APPARENT_POWER.publish(apparentPower);
|
||||
}
|
||||
/**
|
||||
* The current apparent power.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: PAPP field, with a precision of 10 VA.</li>
|
||||
* <li>Standard mode: SINSTS field, with a precision of 1 VA.</li>
|
||||
* </ul>
|
||||
* @return the current apparent power, in Volt Ampere (VA).
|
||||
*/
|
||||
public synchronized Integer getApparentPower() { return apparentPower; }
|
||||
|
||||
|
||||
private Integer maxPowerToday = null;
|
||||
private Long maxPowerTimeToday = null;
|
||||
private void updateMaxPowerToday(long t, String i) {
|
||||
maxPowerToday = Integer.parseInt(i);
|
||||
maxPowerTimeToday = t;
|
||||
}
|
||||
/**
|
||||
* The max apparent power of today.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided.</li>
|
||||
* <li>Standard mode: SMAXSN field.</li>
|
||||
* </ul>
|
||||
* @return the max apparent power of today, in Volt Ampere (VA).
|
||||
*/
|
||||
public synchronized Integer getMaxPowerToday() { return maxPowerToday; }
|
||||
/**
|
||||
* The time when the max apparent power of today was registered.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided.</li>
|
||||
* <li>Standard mode: SMAXSN datetime field.</li>
|
||||
* </ul>
|
||||
* @return the millisecond epoch time of when the max apparent power of today was registered.
|
||||
*/
|
||||
public synchronized Long getMaxPowerTimeToday() { return maxPowerTimeToday; }
|
||||
|
||||
|
||||
private Integer maxPowerYesterday = null;
|
||||
private Long maxPowerTimeYesterday = null;
|
||||
private void updateMaxPowerYesterday(long t, String i) {
|
||||
maxPowerYesterday = Integer.parseInt(i);
|
||||
maxPowerTimeYesterday = t;
|
||||
}
|
||||
/**
|
||||
* The max apparent power of yesterday.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided.</li>
|
||||
* <li>Standard mode: SMAXSN-1 field.</li>
|
||||
* </ul>
|
||||
* @return the max apparent power of yesterday, in Volt Ampere (VA).
|
||||
*/
|
||||
public synchronized Integer getMaxPowerYesterday() { return maxPowerYesterday; }
|
||||
/**
|
||||
* The time when the max apparent power of today was registered.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided.</li>
|
||||
* <li>Standard mode: SMAXSN-1 datetime field.</li>
|
||||
* </ul>
|
||||
* @return the millisecond epoch time of when the max apparent power of today was registered.
|
||||
*/
|
||||
public synchronized Long getMaxPowerTimeYesterday() { return maxPowerTimeYesterday; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private int totalIndex = 0;
|
||||
private final int[] indexes = new int[10];
|
||||
private final String[] indexNames = new String[10];
|
||||
private int currentIndex = 0;
|
||||
|
||||
private void updateIndexHist(String k, String v) {
|
||||
int i = getIndexIdFromHistId(k);
|
||||
indexes[i] = Integer.parseInt(v);
|
||||
indexNames[i] = k;
|
||||
|
||||
int oldTotalIndex = totalIndex;
|
||||
|
||||
totalIndex = 0;
|
||||
for (int idx : indexes)
|
||||
totalIndex += idx;
|
||||
|
||||
if (totalIndex != oldTotalIndex)
|
||||
MQTTSender.INDEX_TOTAL.publish(totalIndex);
|
||||
}
|
||||
private void updateCurrIndexHist(String i) {
|
||||
currentIndex = getIndexIdFromHistId(i);
|
||||
}
|
||||
private static int getIndexIdFromHistId(String i) {
|
||||
switch (i) {
|
||||
case "HCHP": // index name
|
||||
case "EJPHPM": // index name
|
||||
case "BBRHPJB": // index name
|
||||
case "HP..": // PTEC value
|
||||
case "PM..": // PTEC value
|
||||
case "HPJB": // PTEC value
|
||||
return 1;
|
||||
case "BBRHCJW": // index name
|
||||
case "HCJW": // PTEC value
|
||||
return 2;
|
||||
case "BBRHPJW": // index name
|
||||
case "HPJW": // PTEC value
|
||||
return 3;
|
||||
case "BBRHCJR": // index name
|
||||
case "HCJR": // PTEC value
|
||||
return 4;
|
||||
case "BBRHPJR": // index name
|
||||
case "HPJR": // PTEC value
|
||||
return 5;
|
||||
}
|
||||
return 0; // BASE, HCHC, EJPHN, BBRHCJB ; TH.., HC.., HN.., HCJB
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void updateIndexStd(String k, String v) { indexes[Integer.parseInt(k)-1] = Integer.parseInt(v); }
|
||||
private void updateTotalIndexStd(String v) {
|
||||
int oldTotalIndex = totalIndex;
|
||||
|
||||
totalIndex = Integer.parseInt(v);
|
||||
|
||||
if (totalIndex != oldTotalIndex)
|
||||
MQTTSender.INDEX_TOTAL.publish(totalIndex);
|
||||
}
|
||||
private void updateCurrentIndexId(String v) { currentIndex = Integer.parseInt(v) - 1; }
|
||||
private void updateNameOfCurrentIndex(String v) { indexNames[currentIndex] = v; }
|
||||
|
||||
|
||||
/**
|
||||
* List of the 10 indexes managed by the electric meter.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: fields with various names. All the indexes are not transmitted, so others indexes are considered 0.</li>
|
||||
* <li>Standard mode: EASFxx fields.</li>
|
||||
* </ul>
|
||||
* @return the full list of indexes, all measured in Watt hour (Wh)
|
||||
*/
|
||||
public synchronized int[] getIndexes() { return Arrays.copyOf(indexes, indexes.length); }
|
||||
/**
|
||||
* List of the names for each indexes.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: the name of an index is based on the name of the TIC field that provide the index.</li>
|
||||
* <li>Standard mode: the name provided by the field LTARF is applied to the current running index specified by NTARF.</li>
|
||||
* </ul>
|
||||
* @return the full list of name for each indexes. Unknown names are null.
|
||||
*/
|
||||
public synchronized String[] getIndexNames() { return Arrays.copyOf(indexNames, indexNames.length); }
|
||||
public synchronized int getIndex(int i) { return indexes[i]; }
|
||||
public synchronized String getIndexName(int i) { return indexNames[i]; }
|
||||
public synchronized int getIndexCount() { return indexes.length; }
|
||||
public synchronized int getTotalIndex() { return totalIndex; }
|
||||
public synchronized int getCurrentIndex() { return currentIndex; }
|
||||
|
||||
|
||||
|
||||
|
||||
private final int[] distributorIndexes = new int[4];
|
||||
private int currentDistributorIndex = 0;
|
||||
private void updateDistributorIndex(String k, String v) { distributorIndexes[Integer.parseInt(k)-1] = Integer.parseInt(v); }
|
||||
/**
|
||||
* List of the 4 distributor indexes managed by the electric meter.
|
||||
* <p>
|
||||
* These indexes are not related to the pricing applied for the customer.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided.</li>
|
||||
* <li>Standard mode: EASDxx fields.</li>
|
||||
* </ul>
|
||||
* @return the full list of distributor indexes, all measured in Watt hour (Wh). In historical mode, all values are 0.
|
||||
*/
|
||||
public synchronized int[] getDistributorIndexes() { return Arrays.copyOf(distributorIndexes, distributorIndexes.length); }
|
||||
public synchronized int getDistributorIndex(int i) { return distributorIndexes[i]; }
|
||||
public synchronized int getDistributorIndexCount() { return distributorIndexes.length; }
|
||||
public synchronized int getCurrentDistributorIndex() { return currentDistributorIndex; }
|
||||
|
||||
|
||||
|
||||
|
||||
private Integer rmsCurrent = null;
|
||||
private void updateRMSCurrent(String i) { rmsCurrent = Integer.parseInt(i); }
|
||||
public synchronized Integer getRMSCurrent() { return rmsCurrent; }
|
||||
|
||||
|
||||
|
||||
private Integer rmsVoltage = null;
|
||||
private void updateRMSVoltage(String i) { rmsVoltage = Integer.parseInt(i); }
|
||||
public synchronized Integer getRMSVoltage() { return rmsVoltage; }
|
||||
|
||||
|
||||
|
||||
private Integer avgVoltage = null;
|
||||
private Long avgVoltageTime = null;
|
||||
private void updateAvgVoltage(long t, String i) {
|
||||
avgVoltage = Integer.parseInt(i);
|
||||
avgVoltageTime = t;
|
||||
}
|
||||
public synchronized Integer getAvgVoltage() { return avgVoltage; }
|
||||
public synchronized Long getAvgVoltageLastUpdate() { return avgVoltageTime; }
|
||||
|
||||
|
||||
|
||||
private Integer providerCalendarDayToday = null;
|
||||
private Integer providerCalendarDayTomorrow = null;
|
||||
private void updateProviderCalDayToday(String d) { providerCalendarDayToday = Integer.parseInt(d); }
|
||||
private void updateProviderCalDayTomorrow(String d) { providerCalendarDayTomorrow = Integer.parseInt(d); }
|
||||
public synchronized Integer getProviderCalendarDayToday() { return providerCalendarDayToday; }
|
||||
public synchronized Integer getProviderCalendarDayTomorrow() { return providerCalendarDayTomorrow; }
|
||||
|
||||
|
||||
|
||||
|
||||
private String message1 = null, message2 = null;
|
||||
private void updateMessage1(String m) { message1 = m.trim(); }
|
||||
private void updateMessage2(String m) { message2 = m == null ? null : m.trim(); }
|
||||
public synchronized String getMessage1() { return message1; }
|
||||
public synchronized String getMessage2() { return message2; }
|
||||
|
||||
|
||||
|
||||
private boolean[] relaysClosed = null;
|
||||
private void updateRelaysStatus(String r) {
|
||||
int reg = Integer.parseInt(r);
|
||||
relaysClosed = new boolean[8];
|
||||
for (int i = 0; i < 8; i++) {
|
||||
relaysClosed[i] = (reg & 1) == 1;
|
||||
reg >>= 1;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The status of the relays.
|
||||
* <p>
|
||||
* The relay id is 0 based (first relay is 0)
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided.</li>
|
||||
* <li>Standard mode: RELAIS field.</li>
|
||||
* </ul>
|
||||
* @param r the 0-based relay id (from 0 to 7). The relay 0 is the physical relay in the electric meter.
|
||||
* @return true if the specified relay is closed (the current goes through), false if the TIC says otherwise, or null if TIC does not provide the information.
|
||||
*/
|
||||
public synchronized Boolean isRelayClosed(int r) {
|
||||
if (relaysClosed == null || r < 0 || r >= relaysClosed.length)
|
||||
return null;
|
||||
return relaysClosed[r];
|
||||
}
|
||||
/**
|
||||
* The status of the relays.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: not provided.</li>
|
||||
* <li>Standard mode: RELAIS field.</li>
|
||||
* </ul>
|
||||
* @return the status of all the relays. The relay 0 is the physical one. For each relay, true means closed (the current goes through), and false means opened.
|
||||
*/
|
||||
public synchronized boolean[] getRelaysStatus() {
|
||||
return relaysClosed;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private CutRelayStatus cutRelayStatus = null;
|
||||
private Boolean providerCoverOpened = null;
|
||||
private Boolean overVoltage = null;
|
||||
private Boolean overPowerConsumption = null;
|
||||
private Boolean producerMode = null;
|
||||
private Boolean activeEnergyNegative = null;
|
||||
private Boolean internalClockBad = null;
|
||||
private EuridisOutputStatus euridisOutputStatus = null;
|
||||
private CPLStatus cplStatus = null;
|
||||
private Boolean cplSynchronized = null;
|
||||
private void updateStatusRegister(String r) {
|
||||
int reg = Integer.parseInt(r, 16);
|
||||
|
||||
int isOffPeakRelayOpened = reg & 0x1; reg >>= 1;
|
||||
int cutRelayStatusRaw = reg & 0x7; reg >>= 3;
|
||||
int isProviderCoverOpened = reg & 0x1; reg >>= 1;
|
||||
/* unused bit */ reg >>= 1;
|
||||
int isOverVoltage = reg & 0x1; reg >>= 1;
|
||||
int isOverPowerConsumption = reg & 0x1; reg >>= 1;
|
||||
int isProducerMode = reg & 0x1; reg >>= 1;
|
||||
int isActiveEnergyNegative = reg & 0x1; reg >>= 1;
|
||||
int currentActiveIndex = reg & 0xF; reg >>= 4;
|
||||
int currentDistIndexRaw = reg & 0x3; reg >>= 2;
|
||||
int isInternalClockBad = reg & 0x1; reg >>= 1;
|
||||
int isTICModeStandard = reg & 0x1; reg >>= 1;
|
||||
/* unused bit */ reg >>= 1;
|
||||
int euridisOutputStatusRaw = reg & 0x3; reg >>= 2;
|
||||
int cplStatusRaw = reg & 0x3; reg >>= 2;
|
||||
int isCPLSynchronized = reg & 0x1; reg >>= 1;
|
||||
int tempoCurrentDayColorRaw= reg & 0x3; reg >>= 2;
|
||||
int tempoNextDayColorRaw = reg & 0x3; reg >>= 2;
|
||||
//int mobilePointNotice = reg & 0x3; reg >>= 2;
|
||||
//int currentMobilePoint = reg & 0x3; reg >>= 2;
|
||||
|
||||
|
||||
if (isRelayClosed(0) != null && ((isOffPeakRelayOpened == 0) != isRelayClosed(0))) {
|
||||
Log.warn("Inconsistent status of relay 0 between RELAIS data set and value in STGE data set."
|
||||
+ " RELAIS says relay 0 is " + (isRelayClosed(0) ? "closed" : "opened") + " and"
|
||||
+ " STGE ’s bit 0 says off peak relay is " + (isOffPeakRelayOpened == 0 ? "closed" : "opened") + ".");
|
||||
}
|
||||
|
||||
cutRelayStatus = cutRelayStatusRaw < CutRelayStatus.values().length ? CutRelayStatus.values()[cutRelayStatusRaw] : null;
|
||||
providerCoverOpened = isProviderCoverOpened == 1;
|
||||
overVoltage = isOverVoltage == 1;
|
||||
overPowerConsumption = isOverPowerConsumption == 1;
|
||||
producerMode = isProducerMode == 1;
|
||||
activeEnergyNegative = isActiveEnergyNegative == 1;
|
||||
|
||||
if (currentActiveIndex != currentIndex) {
|
||||
Log.warn("Inconsistent current index id between NTARF data set value and current active index value in register in STGE data set");
|
||||
}
|
||||
|
||||
currentDistributorIndex = currentDistIndexRaw;
|
||||
internalClockBad = isInternalClockBad == 1;
|
||||
|
||||
if (isTICModeStandard != 1) {
|
||||
Log.warn("Register in STGE data set declared TIC mode as historical but we are processing the data frame as standard mode. (STGE is only send in standard mode, btw)");
|
||||
}
|
||||
|
||||
euridisOutputStatus = euridisOutputStatusRaw < EuridisOutputStatus.values().length ? EuridisOutputStatus.values()[euridisOutputStatusRaw] : null;
|
||||
cplStatus = cplStatusRaw < CPLStatus.values().length ? CPLStatus.values()[cplStatusRaw] : null;
|
||||
cplSynchronized = isCPLSynchronized == 1;
|
||||
tempoCurrentDayColor = tempoCurrentDayColorRaw < TempoDayColor.values().length ? TempoDayColor.values()[tempoCurrentDayColorRaw] : null;
|
||||
tempoNextDayColor = tempoNextDayColorRaw < TempoDayColor.values().length ? TempoDayColor.values()[tempoNextDayColorRaw] : null;
|
||||
|
||||
|
||||
}
|
||||
public enum CutRelayStatus {
|
||||
CLOSED, OPENED_OVERPOWER, OPENED_OVERVOLTAGE, OPENED_REMOTE_CONTROL,
|
||||
OPENED_OVERHEAT_WITH_OVERCURRENT, OPENED_OVERHEAT_NO_OVERCURRENT
|
||||
}
|
||||
public enum EuridisOutputStatus {
|
||||
DISABLED, ENABLED_UNSECURED, UNDEFINED, ENABLED_SECURED
|
||||
}
|
||||
public enum CPLStatus { NEW_UNLOCK, NEW_LOCK, REGISTERED }
|
||||
public synchronized CutRelayStatus getCutRelayStatus() { return cutRelayStatus; }
|
||||
public synchronized Boolean isProviderCoverOpened() { return providerCoverOpened; }
|
||||
public synchronized Boolean isOverVoltage() { return overVoltage; }
|
||||
public synchronized Boolean isOverPowerConsumption() { return overPowerConsumption; }
|
||||
public synchronized Boolean isProducerMode() { return producerMode; }
|
||||
public synchronized Boolean isActiveEnergyNegative() { return activeEnergyNegative; }
|
||||
public synchronized Boolean isInternalClockBad() { return internalClockBad; }
|
||||
public synchronized EuridisOutputStatus getEuridisOutputStatus() { return euridisOutputStatus; }
|
||||
public synchronized CPLStatus getCPLStatus() { return cplStatus; }
|
||||
public synchronized Boolean isCPLSynchronized() { return cplSynchronized; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private TempoDayColor tempoCurrentDayColor = null;
|
||||
private TempoDayColor tempoNextDayColor = null;
|
||||
private void updateDEMAIN(String o) {
|
||||
if (o.equals("----"))
|
||||
tempoNextDayColor = TempoDayColor.UNKNOWN;
|
||||
else if (o.equals("BLEU"))
|
||||
tempoNextDayColor = TempoDayColor.BLUE;
|
||||
else if (o.equals("BLAN"))
|
||||
tempoNextDayColor = TempoDayColor.WHITE;
|
||||
else if (o.equals("ROUG"))
|
||||
tempoNextDayColor = TempoDayColor.RED;
|
||||
else
|
||||
tempoNextDayColor = null;
|
||||
}
|
||||
public enum TempoDayColor { UNKNOWN, BLUE, WHITE, RED }
|
||||
public synchronized TempoDayColor getTempoCurrentDayColor() { return tempoCurrentDayColor; }
|
||||
public synchronized TempoDayColor getTempoNextDayColor() { return tempoNextDayColor; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private List<DayScheduleTimeSlot> nextDayScheduling = null;
|
||||
private void updateNextDayScheduling(String d) {
|
||||
String[] rawSlots = d.split(" ");
|
||||
nextDayScheduling = new ArrayList<>();
|
||||
for (String rawSlot : rawSlots) {
|
||||
if (rawSlot.length() != 8 || rawSlot.equals("NONUTILE"))
|
||||
continue;
|
||||
|
||||
int hour = Integer.parseInt(rawSlot.substring(0, 2));
|
||||
int minute = Integer.parseInt(rawSlot.substring(2, 4));
|
||||
|
||||
int reg = Integer.parseInt(rawSlot.substring(4), 16);
|
||||
|
||||
int indexChangeRaw = reg & 0xF; reg >>= 4;
|
||||
boolean[] virtRelays = new boolean[7];
|
||||
for (int i = 0; i < 7; i++) {
|
||||
virtRelays[i] = (reg & 0x1) == 1;
|
||||
reg >>= 1;
|
||||
}
|
||||
/* unused bits */ reg >>= 3;
|
||||
int physRelayChange = reg & 0x3; reg >>= 2;
|
||||
|
||||
nextDayScheduling.add(new DayScheduleTimeSlot(
|
||||
minute * 60_000L + hour * 3_600_000L,
|
||||
PhysicalRelayChange.values()[physRelayChange],
|
||||
indexChangeRaw >= 1 && indexChangeRaw <= 10 ? (indexChangeRaw - 1) : null,
|
||||
virtRelays));
|
||||
}
|
||||
}
|
||||
public static class DayScheduleTimeSlot {
|
||||
public final long millisDayStart;
|
||||
public final PhysicalRelayChange physicalRelayChange;
|
||||
public final Integer changeToIndex;
|
||||
public final boolean[] virtualRelaysStatus;
|
||||
|
||||
public DayScheduleTimeSlot(long start, PhysicalRelayChange physRelay, Integer indexChange, boolean[] virtRelays) {
|
||||
millisDayStart = start;
|
||||
physicalRelayChange = physRelay;
|
||||
changeToIndex = indexChange;
|
||||
virtualRelaysStatus = virtRelays;
|
||||
}
|
||||
|
||||
public enum PhysicalRelayChange { NO_CHANGE, TEMPO_OR_NO_CHANGE, OPEN, CLOSE }
|
||||
}
|
||||
public synchronized List<DayScheduleTimeSlot> getNextDayScheduling() { return nextDayScheduling; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private String MOTDETAT = null;
|
||||
private void updateMOTDETAT(String o) { MOTDETAT = o; }
|
||||
/**
|
||||
* The value of MOTDETAT from the historical TIC data frame.
|
||||
* <p>
|
||||
* The interpretation of the data is not documented.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: MOTDETAT field.</li>
|
||||
* <li>Standard mode: not provided.</li>
|
||||
* </ul>
|
||||
* @return the raw value of MOTDETAT, or null if absent.
|
||||
* @deprecated this value is only supported on historical TIC mode, and its interpretation is not documented.
|
||||
*/
|
||||
@Deprecated
|
||||
public synchronized String getMOTDETAT() { return MOTDETAT; }
|
||||
|
||||
|
||||
|
||||
private String HHPHC = null;
|
||||
private void updateHHPHC(String o) { HHPHC = o; }
|
||||
/**
|
||||
* The value of HHPHC from the historical TIC data frame, indicating the time in the day for the .
|
||||
* <p>
|
||||
* The interpretation of the data is not documented.
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: HHPHC field.</li>
|
||||
* <li>Standard mode: not provided.</li>
|
||||
* </ul>
|
||||
* @return the raw value of HHPHC, or null if absent.
|
||||
* @deprecated this value is only supported on historical TIC mode, and its interpretation is not documented.
|
||||
*/
|
||||
@Deprecated
|
||||
public synchronized String getHHPHC() { return HHPHC; }
|
||||
|
||||
|
||||
|
||||
|
||||
private boolean histMobilePointNotice = false;
|
||||
/**
|
||||
* The notice for the upcoming mobile point (EJP subscription).
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Historical mode: presence of PEJP field, during 30 minutes before the start of the mobile point.</li>
|
||||
* <li>Standard mode: not provided.</li>
|
||||
* </ul>
|
||||
* @return true if the TIC is in historical mode and a mobile point will start in the next 30 minutes, false otherwise.
|
||||
* @deprecated this value is only supported on historical TIC mode.
|
||||
*/
|
||||
@Deprecated
|
||||
public synchronized boolean getHistMobilePointNotice() { return histMobilePointNotice; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
296
src/main/java/fr/mbaloup/home/tic/TICRawDecoder.java
Normal file
296
src/main/java/fr/mbaloup/home/tic/TICRawDecoder.java
Normal file
@ -0,0 +1,296 @@
|
||||
package fr.mbaloup.home.tic;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import org.rapidoid.log.Log;
|
||||
|
||||
public class TICRawDecoder extends Thread {
|
||||
|
||||
static final char FRAME_START = 0x02;
|
||||
static final char FRAME_END = 0x03;
|
||||
static final char TRANSMISSION_END = 0x04;
|
||||
static final char DATASET_START = 0x0A;
|
||||
static final char DATASET_END = 0x0D;
|
||||
static final char SEP_SP = 0x20;
|
||||
static final char SEP_HT = 0x09;
|
||||
|
||||
static final String INPUT_PATH = System.getenv("TIC_TTY_INPUT");
|
||||
|
||||
static final int CORRUPTION_MAX_LEVEL = 10; // max number of corruption detected while reading raw input
|
||||
static final long CORRUPTION_LEVEL_DECAY_INTERVAL = 5000; // interval in ms between each corruption level decrement
|
||||
|
||||
private InputStream input;
|
||||
|
||||
private int corruptedDataCount = 0;
|
||||
private long lastCorruptedData = System.currentTimeMillis();
|
||||
|
||||
private final BlockingQueue<DataFrame> outQueue = new LinkedBlockingQueue<>(10000);
|
||||
|
||||
|
||||
public TICRawDecoder() throws IOException {
|
||||
super("TIC Input Thread");
|
||||
configureInput();
|
||||
start();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void configureInput() throws IOException {
|
||||
input = new FileInputStream(INPUT_PATH);
|
||||
//input = new SimulatedTICInputStream();
|
||||
}
|
||||
|
||||
|
||||
private void signalCorruptedInput(String desc) {
|
||||
corruptedDataCount++;
|
||||
lastCorruptedData = System.currentTimeMillis();
|
||||
Log.warn("Raw input corruption detected (" + corruptedDataCount + "): " + desc);
|
||||
}
|
||||
|
||||
private boolean checkCorruptedInputStatus() throws IOException {
|
||||
if (corruptedDataCount > 0) {
|
||||
if (lastCorruptedData < System.currentTimeMillis() - 5000) {
|
||||
corruptedDataCount--;
|
||||
lastCorruptedData = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
if (corruptedDataCount > 10) {
|
||||
Log.warn("Raw input corruption is too high, reopening the input stream... ");
|
||||
try {
|
||||
input.close();
|
||||
try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
|
||||
configureInput();
|
||||
}
|
||||
finally {
|
||||
corruptedDataCount = 0;
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Map<String, DataSet> frameData = null;
|
||||
long frameTime = -1;
|
||||
StringBuilder dsBuffer = new StringBuilder(32);
|
||||
boolean processingDS = false;
|
||||
|
||||
Log.info("Decoder is now running.");
|
||||
|
||||
for(;;) {
|
||||
if (checkCorruptedInputStatus()) {
|
||||
frameData = null;
|
||||
dsBuffer.setLength(0);
|
||||
processingDS = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
char c = (char) input.read();
|
||||
|
||||
// handle new frame
|
||||
if (c == FRAME_START) {
|
||||
frameData = new LinkedHashMap<>();
|
||||
frameTime = System.currentTimeMillis();
|
||||
continue;
|
||||
}
|
||||
// skip all data encountered before our first FRAME_START
|
||||
if (frameData == null)
|
||||
continue;
|
||||
|
||||
// handle frame end
|
||||
if (c == FRAME_END || c == TRANSMISSION_END) {
|
||||
if (!frameData.isEmpty()) {
|
||||
outQueue.add(new DataFrame(frameTime, frameData));
|
||||
}
|
||||
else {
|
||||
signalCorruptedInput("FRAME_END or TRANSMISSION_END encountered with not data frame registered.");
|
||||
}
|
||||
frameData = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// infogroup start
|
||||
if (c == DATASET_START) {
|
||||
if (processingDS) // should not happened
|
||||
signalCorruptedInput("DATASET_START encountered while already decoding an info group.");
|
||||
processingDS = true;
|
||||
dsBuffer.setLength(0);
|
||||
continue;
|
||||
}
|
||||
// infogroup end
|
||||
if (c == DATASET_END) {
|
||||
if (dsBuffer.length() >= 3) {
|
||||
try {
|
||||
String[] dsData = toDataSetStrings(dsBuffer.toString());
|
||||
if (dsData != null) {
|
||||
if (dsData.length == 2) {
|
||||
frameData.put(dsData[0], new DataSet(null, dsData[1]));
|
||||
}
|
||||
else if (dsData.length == 3) {
|
||||
frameData.put(dsData[0], new DataSet(DataConverter.fromDateToMillis(dsData[1]), dsData[2]));
|
||||
}
|
||||
else {
|
||||
signalCorruptedInput("Data set content corrupted (invalid format).");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
signalCorruptedInput("Error while decoding data set: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
else {
|
||||
signalCorruptedInput("DATASET_END encountered too soon.");
|
||||
}
|
||||
processingDS = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// skip all data outside infogroups
|
||||
if (!processingDS) {
|
||||
signalCorruptedInput("Expected DATASET_START, but received regular character.");
|
||||
continue;
|
||||
}
|
||||
|
||||
dsBuffer.append(c);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.error("Error while reading raw TIC input", e);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
int checksumMode = 0;
|
||||
char prevSeparator = 0x00;
|
||||
|
||||
private String[] toDataSetStrings(String buff) {
|
||||
|
||||
char checksum = buff.charAt(buff.length() - 1);
|
||||
char separator = buff.charAt(buff.length() - 2);
|
||||
|
||||
if (separator != SEP_SP && separator != SEP_HT) {
|
||||
signalCorruptedInput("Invalid info group separator in buffer " + hexdump(buff));
|
||||
return null;
|
||||
}
|
||||
|
||||
// check separator
|
||||
if (prevSeparator == 0x00)
|
||||
prevSeparator = separator;
|
||||
else if (separator != prevSeparator) {
|
||||
signalCorruptedInput("Received separator is not the same as previously received. Buffer is " + hexdump(buff));
|
||||
return null;
|
||||
}
|
||||
|
||||
// check checksum
|
||||
if (checksumMode == 0) {
|
||||
char cs1 = checksum(buff.substring(0, buff.length() - 2));
|
||||
char cs2 = checksum(buff.substring(0, buff.length() - 1));
|
||||
if (checksum == cs1) {
|
||||
if (checksum != cs2) {
|
||||
checksumMode = 1;
|
||||
}
|
||||
// else {} // ignored, because in this case, both checksum are equals to the received checksum
|
||||
}
|
||||
else {
|
||||
if (checksum == cs2) {
|
||||
checksumMode = 2;
|
||||
}
|
||||
else {
|
||||
signalCorruptedInput("Invalid checksum. Received " + hex(checksum) + " but expected either " + hex(cs1) + " (mode 1) or " + hex(cs2) + " (mode 2). Buffer is " + hexdump(buff));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (checksumMode == 1) {
|
||||
char cs1 = checksum(buff.substring(0, buff.length() - 2));
|
||||
if (checksum != cs1) {
|
||||
signalCorruptedInput("Invalid checksum. Received " + hex(checksum) + " but expected " + hex(cs1) + " (mode 1). Buffer is " + hexdump(buff));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (checksumMode == 2) {
|
||||
char cs2 = checksum(buff.substring(0, buff.length() - 1));
|
||||
if (checksum != cs2) {
|
||||
signalCorruptedInput("Invalid checksum. Received " + hex(checksum) + " but expected " + hex(cs2) + " (mode 2). Buffer is " + hexdump(buff));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// separate key, value and eventually the time data
|
||||
return buff.substring(0, buff.length() - 2).split("" + separator, separator == SEP_SP ? 2 : 3);
|
||||
}
|
||||
|
||||
public static char checksum(String s) {
|
||||
int cs = 0;
|
||||
for (char c : s.toCharArray()) {
|
||||
cs += c;
|
||||
}
|
||||
return (char) ((cs & 0x3F) + 0x20);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private static String hex(char c) {
|
||||
return String.format("0x%02x", (int)c);
|
||||
}
|
||||
|
||||
private static String hexdump(String s) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (char c : s.toCharArray())
|
||||
sb.append(String.format("%02x ", (int)c));
|
||||
sb.append('|');
|
||||
for (char c : s.toCharArray())
|
||||
sb.append((c >= ' ' && c < 0x7f) ? c : '.');
|
||||
sb.append('|');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public DataFrame getNextDataFrame() throws InterruptedException {
|
||||
if (outQueue.size() > 3) {
|
||||
Log.warn("TICDecoder queue is growing (size=" + outQueue.size() + ")");
|
||||
}
|
||||
return outQueue.take();
|
||||
}
|
||||
}
|
58
src/main/java/fr/mbaloup/home/util/TimeUtil.java
Normal file
58
src/main/java/fr/mbaloup/home/util/TimeUtil.java
Normal file
@ -0,0 +1,58 @@
|
||||
package fr.mbaloup.home.util;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
public class TimeUtil {
|
||||
|
||||
|
||||
public static boolean isDifferentDay(long c1, long c2) {
|
||||
return isDifferentDay(getCalendarOfTime(c1), getCalendarOfTime(c2));
|
||||
}
|
||||
|
||||
public static boolean isDifferentDay(Calendar c1, Calendar c2) {
|
||||
return getDayOfMonth(c1) != getDayOfMonth(c2)
|
||||
|| getMonth(c1) != getMonth(c2)
|
||||
|| getYear(c1) != getYear(c2);
|
||||
}
|
||||
|
||||
public static int getNbDayInMonth(Calendar cal) {
|
||||
switch(getMonth(cal)) {
|
||||
case Calendar.JANUARY:
|
||||
case Calendar.MARCH:
|
||||
case Calendar.MAY:
|
||||
case Calendar.JULY:
|
||||
case Calendar.AUGUST:
|
||||
case Calendar.OCTOBER:
|
||||
case Calendar.DECEMBER:
|
||||
return 31;
|
||||
case Calendar.FEBRUARY:
|
||||
return ((GregorianCalendar) cal).isLeapYear(getYear(cal)) ? 29 : 28;
|
||||
default:
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
public static int getDayOfMonth(Calendar cal) {
|
||||
return cal.get(Calendar.DAY_OF_MONTH);
|
||||
}
|
||||
public static int getMonth(Calendar cal) {
|
||||
return cal.get(Calendar.MONTH);
|
||||
}
|
||||
public static int getYear(Calendar cal) {
|
||||
return cal.get(Calendar.YEAR);
|
||||
}
|
||||
|
||||
public static void setMidnight(Calendar cal) {
|
||||
cal.set(Calendar.HOUR_OF_DAY, 0);
|
||||
cal.set(Calendar.MINUTE, 0);
|
||||
cal.set(Calendar.SECOND, 0);
|
||||
cal.set(Calendar.MILLISECOND, 0);
|
||||
}
|
||||
|
||||
public static Calendar getCalendarOfTime(long time) {
|
||||
Calendar cal = new GregorianCalendar();
|
||||
cal.setTimeInMillis(time);
|
||||
return cal;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user