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