diff --git a/INSTALL.md b/INSTALL.md
index ea4def2..497032e 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -1,465 +1,465 @@
# Installation
Deux type d'installation possible :
- Une version Raspbery PI 3B, si vous avez un point wifi actif (même occasionnellement) et que votre matériel solaire est à porté de wifi. C'est une solution plutôt simple (si on touche un peu sous linux).
- Une version Raspbery Pi 0 + Arduino : plus complexe à mettre en oeuvre (il faut savoir souder et avoir plus de connaissance) mais beaucoup plus souple et moins chère. Particulièrement adapté si votre installation réseau est loin (max 60m) de votre maison

PvMonit support tout le matériel Victron compatible Ve Direct (via USB) :
Les fonctionnalités de PvMonit sont dissociable :
* Interface web en temps réel
* Export vers emoncms
* Affichage LCD
#### La base / le socle
Installation de PvMonit via le dépôt git et de ses dépendances :
```bash
-aptitude install php-cli php-yaml git python-serial sudo screen
+apt-get install php-cli php-yaml git python-serial sudo screen
cd /opt
git clone https://github.com/kepon85/PvMonit.git
cd PvMonit
cp config-default.yaml config.yaml
```
Vous pouvez maintenant éditer le fichier config.yaml à votre guise !
### Ve.direct via USB
Dans le fichier config.yaml mentionner :
```yaml
vedirect:
by: usb
```
Test du script vedirect.py : brancher un appareil Victron avec un Câble Ve.Direct USB et voici un exemple de ce que vous devriez obtenir (Ici un MPTT BlueSolare branché sur le ttyUS0)
```
$ /opt/PvMonit/bin/vedirect.py /dev/ttyUSB0
PID:0xA04A
FW:119
SER#:HQ********
V:25660
I:500
VPV:53270
PPV:14
CS:3
ERR:0
LOAD:ON
H19:3348
H20:1
H21:17
H22:33
H23:167
HSDS:52
```
Pour comprendre chaque valeur, téléchargez la documentation *Victron VE Direct Protocol documentation* : https://www.victronenergy.fr/support-and-downloads/whitepapers ([disponible aussi à cet url](https://david.mercereau.info/wp-content/uploads/2019/10/VE.Direct-Protocol.pdf))
Lancer la commande :
```sh
visudo
```
Si vous utilisez l'interface web pvmonit, ajouter :
```diff
+ www-data ALL=(ALL) NOPASSWD:/opt/PvMonit/bin/vedirect.py *
```
Si vous utilisez l'export vers emoncms, ajouter :
```diff
+ pvmonit ALL=(ALL) NOPASSWD:/opt/PvMonit/bin/vedirect.py *
```
### Ve.direct via Arduino
Avec l'Arduino IDE, uploader le firmware "ArduinoMegaVeDirect.ino" contenu dans le dossier "firmware"
Faites vos câble ve.direct avec les connecteur JST-PH. De la documentation à ce sujet :
- https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable
- http://www.svpartyoffive.com/2018/02/28/victron-monitors-technical/
- https://store.volts.ca/media/attachment/file/v/e/ve.direct-protocol-3.23.pdf
- http://jeperez.com/connect-bmv-victron-computer/
- http://www.mjlorton.com/forum/index.php?topic=238.0
- BMV e 3,3V : https://github.com/winginitau/VictronVEDirectArduino
- https://www.victronenergy.com/live/vedirect_protocol:faq#q4is_the_vedirect_interface_33_or_5v
Conseil : utiliser des connecteur MOLEX (pratique pour que les câbles soit dé-connectable) : https://arduino103.blogspot.com/2013/07/connecteur-molex-comment-utiliser-le-kit.html
Connecté l'arduino en série (utiliser 3 fils d'un câble téléphonie/RJ45) avec le raspbery pi comme sur le schéma ci-après :

Sur le Raspbery pi, il faut que le port série soit actif
```bash
raspi-config
# Interfacing Option / P6 Serial /
# Login shell : NO
# Serial port harware enable : Yes
reboot
```
Pour être sûr que cela fonctionne vous pouvez lancer la commande suivante sur le pi :
```bash
screen /dev/ttyAMA0 4800
```
Vous devriez obtenir quelque chose comme :
```
S:3_P -95
S:3_CE -39101
S:3_SOC 889
S:3_TTG 1597
S:3_Alarm OFF
S:3_Relay OFF
S:3_AR 0
S:3_BMV 700
S:3_FW 0308
S:3_Checksum 8
S:3_H1 -102738
S:3_H2 -45215
S:3_H3 -102738
S:3_H4 1
S:3_H5 0
S:3_H6 -21450007
S:3_H7 21238
S:3_H8 29442
S:3_H9 362593
S:3_H10 103
S:3_H11 0
S:3_H12 0
S:3_H17 53250
S:3_H18 62805
S:3_Checksum �
S:3_PID 0x203
STOP
```
Dans le fichier config.yaml mentionner :
```yaml
vedirect:
by: arduino
```
```bash
apt-get install python3 python3-pip python3-yaml
pip3 install pyserial
```
Ajouter dans le fichier /etc/rc.local :(avant le exit 0)
```bash
screen -A -m -d -S arduino /opt/PvMonit/bin/getSerialArduino-launch.sh
```
Vous pouvez le lancer "à la main" avec la commande :
```bash
python3 /opt/PvMonit/bin/getSerialArduino.py
```
Et vous assurez que le fichier /tmp/PvMonit_getSerialArduino.data.yaml existe bien et que les données sont justes.
#### Interface web en temps réel
Installation des dépendances :
```bash
aptitude install lighttpd php-cgi php-xml php7.3-json
lighttpd-enable-mod fastcgi
lighttpd-enable-mod fastcgi-php
```
Configuration du serveur http, avec le fichier /etc/lighttpd/lighttpd.conf :
```diff
- server.document-root = "/var/www/html/"
+ server.document-root = "/opt/PvMonit/www/"
```
On applique la configuration :
```bash
service lighttpd restart
```
C'est terminé, vous pouvez vous connecter sur votre IP local pour joindre votre serveur web :
Attention : dans la configuration l'appel du fichier data (urlDataXml) doit contenir un nom de domaine, quand vous joingné l'interface ce nom de domaine doit être identique à celui. Exemple vous ateignez l'interface par pv.chezmoi.fr, dans urlDataXml il doit y avoir urlDataXml: http://pv.chezmoi.fr/data-xml.php (modifier le fichier /etc/hosts au besoin...)

#### Export vers emoncms
Connectez-vous à votre interface emoncms hébergée ou créez un compte sur [emoncms.org](https://emoncms.org/) et rendez-vous sur la page "Input api" https://emoncms.org/input/api :

Récupérez la valeur "Accès en écriture" et ajoutez-la dans le fichier de configuration Pvmonit */opt/PvMonit/config.yaml* :
```yaml
emoncms:
urlInputJsonPost: https://emoncms.org/input/post.json
apiKey: XXXXXXXXXXXXXXXXXXXXXXXX
```
Création d'un utilisateur dédié avec pouvoir restreint
```bash
adduser --shell /bin/bash pvmonit
```
Installation des dépendances :
```bash
aptitude install lynx
```
Test de collecte :
```
$ su - pvmonit -c /opt/PvMonit/getForEmoncms.php
2016-11-02T10:55:30+01:00 - C'est un MPTT, modèle "BlueSolar MPPT 100/30 rev2" du nom de MpttBleu
2016-11-02T10:55:30+01:00 - Les données sont formatées comme ceci : V:26180,I:800,VPV:56360,PPV:21,CS:3,ERR:0,H19:3352,H20:5,H21:51,H22:33,H23:167
2016-11-02T10:55:31+01:00 - C'est un MPTT, modèle "BlueSolar MPPT 100/30 rev2" du nom de MpttBlanc
2016-11-02T10:55:31+01:00 - Les données sont formatées comme ceci : V:26200,I:600,VPV:53630,PPV:18,CS:3,ERR:0,H19:1267,H20:4,H21:46,H22:17,H23:201
2016-11-02T10:55:31+01:00 - Après correction, la température est de 11.88°C
2016-11-02T10:55:31+01:00 - Tentative 1 de récupération de consommation
2016-11-02T10:55:32+01:00 - Trouvé à la tentative 1 : la La consommation trouvé est 00.1A
2016-11-02T10:55:32+01:00 - La consommation est de 00.1A soit 23W
```
Test d'envoi des données :
```
$ su - pvmonit -c /opt/PvMonit/sendToEmoncms.php
2016-11-02T10:56:44+01:00 - Données correctements envoyées : 1, données en erreurs : 0
```
Mettre les scripts en tâche planifiée
```bash
crontab -e -u pvmonit
```
Ajouter :
```diff
+# Script de récupération des données, toutes les 5 minutes
+/5 * * * * /usr/bin/php /opt/PvMonit/getForEmoncms.php >> /tmp/PvMonit.getForEmoncms.log
+# Script d'envoi des données, ici toutes les 1/2 heures
+3,33 * * * * /usr/bin/php /opt/PvMonit/sendToEmoncms.php >> /tmp/PvMonit.sendToEmoncms.log
```
Je n'explique pas ici comment configurer emoncms, les flux pour obtenir de beaux dashboard, je vous laisse lire la documentation...

Voici, pour exemple, mon dashboard : http://emoncms.mercereau.info/dashboard/view?id=1
Une capture :

#### Sonde température/humidité (DHT) sur GPIO (sur raspberi pi)
Installation des dépendances :
```
pip3 install Adafruit_DHT
```
Lancer la commande :
```sh
visudo
```
Si vous utilisez l'interface web pvmonit, ajouter :
```diff
+ www-data ALL=(ALL) NOPASSWD:/usr/bin/python3 /opt/PvMonit/bin/DHT.py *
```
Si vous utilisez l'export vers emoncms, ajouter :
```diff
+ pvmonit ALL=(ALL) NOPASSWD: /usr/bin/python3 /opt/PvMonit/bin/DHT.py *
```
Puis activer la sonde :
```bash
ln -s /opt/PvMonit/bin-available/DhtGpio.php /opt/PvMonit/bin-enabled/OTHER-THome.php
```
#### Sonde température/humidité (DHT) récupéré sur l'arduino
/!\ Uniquement si vous avez un Arduino pour récolter les donnée
```bash
ln -s /opt/PvMonit/bin-available/TempHumByArduino.php /opt/PvMonit/bin-enabled/OTHER-TSol.php
```
#### Sonde de courant (type ACS712) récupéré sur l'arduino
/!\ Uniquement si vous avez un Arduino pour récolter les donnée
```bash
ln -s /opt/PvMonit/bin-available/CurrentByArduino.php/opt/PvMonit/bin-enabled/OTHER-CONSO.php
```
#### Sonde température USB (option)
La sonde *thermomètre USB TEMPer*, cette sonde fonctionne avec le logiciel temperv14 qui est plutôt simple à installer
```bash
apt-get install libusb-dev libusb-1.0-0-dev unzip
cd /opt
wget http://dev-random.net/wp-content/uploads/2013/08/temperv14.zip
#ou un miroir
#wget http://www.generation-linux.fr/public/juin14/temperv14.zip
unzip temperv14.zip
cd temperv14/
make
```
Test de la sonde :
```bash
$ /opt/temperv14/temperv14 -c
18.50
```
On ajoute ensuite la possibilité à des utilisateurs "restrint" d'exécutant de lancer les script avec sudo sans mot de passe :
Lancer la commande :
```sh
visudo
```
Si vous utilisez l'interface web pvmonit, ajouter :
```diff
+ www-data ALL=(ALL) NOPASSWD: /opt/temperv14/temperv14 -c
```
Si vous utilisez l'export vers emoncms, ajouter :
```diff
+ pvmonit ALL=(ALL) NOPASSWD: /opt/temperv14/temperv14 -c
```
Activer le script (et l'éditer au besoin)
```bash
ln -s /opt/PvMonit/bin-available/TemperatureUSB.php /opt/PvMonit/bin-enabled/other-TEMP.php
```
Autres documentations à propos de cette sonde :
- http://www.generation-linux.fr/index.php?post/2014/06/21/Relever-et-grapher-la-temp%C3%A9rature-de-sa-maison-sur-Debian
- http://dev-random.net/temperature-measuring-using-linux-and-raspberry-pi/
#### Pince ampèremétrique USB (option)
/!\ Uniquement si vous n'avez pas d'Arduino
J'utilise la pince ampèremétrique USB Aviosys 8870 pour mesurer ma consommation électrique.
Le petit script perl (/opt/PvMonit/bin/ampermetre.pl) est très simple pour lire la pince ampèremétrique qui sera branchée en USB et apparaîtra dans votre système sur le port /dev/ttyACM0
Celui-ci dépend de la librairie serialport :
```bash
aptitde install libdevice-serialport-perl
```
Test : :
```bash
$ /opt/PvMonit/bin-available/ampermetre.pl
00.1A
```
Si vous utilisez l'interface web pvmonit, ajouter :
```diff
+ www-data ALL=(ALL) NOPASSWD: /opt/PvMonit/bin/*
```
Si vous utilisez l'export vers emoncms, ajouter :
```diff
+ pvmonit ALL=(ALL) NOPASSWD: /opt/PvMonit/bin/*
```
Activer le script (et l'éditer au besoin)
```bash
ln -s /opt/PvMonit/bin-available/AmpermetreUSB.php /opt/PvMonit/bin-enabled/other-CONSO.php
```
#### Co² Meter
Il s'agit le ht2000 co² meter. Je ne l'utilise que pour le co² ayant des sondes ailleur mais il peut aussi donner l'humidité et la température. Si vous voulez aussi ces informations vous pouvez regarder de ce côté : https://github.com/tomvanbraeckel/slab_ht2000
Pour ma part (uniquement pour le co²) il faut compile le script :
```bash
cd /opt/PvMonit/bin
gcc ht2000.c -o ht2000
```
Tester :
```bash
./ht2000 /dev/hidraw0
```
Doit retourner une valeur numérique
Ensuite (vu qu'il faut le lancer en root) vous devez le mettre dans le sudo :
Si vous utilisez l'interface web pvmonit, ajouter :
```diff
+ www-data ALL=(ALL) NOPASSWD: /opt/PvMonit/bin/ht2000 *
```
Si vous utilisez l'export vers emoncms, ajouter :
```diff
+ pvmonit ALL=(ALL) NOPASSWD: /opt/PvMonit/bin/ht2000 *
```
#### Raspberry Adafruit LCD RGB - 16x2 + Keypad (option)
Uniquement pour les Raspbery Pi

Permet d'afficher les informations principales sur le raspbery pi (Etat des batteries, puissance en cours...)
```bash
raspi-config
# Interfacing Option / P6 Serial /
# Login shell : NO
# Serial port harware enable : Yes
reboot
aptitude install i2c-tools
i2cdetect 1
```
La dernière commande (i2cdetect 1) doit afficher quelque chose comme :
``` 0 1 2 3 4 5 6 7 8 9 a b c d e f
00: *-- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: 20 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
```
Pour tester le LCD lancer la commande :
```bash
pip3 install adafruit-circuitpython-charlcd lxml
python3 /opt/PvMonit/lcd/lcd.py
```
Pour que le LCD fonctionne au démarrage, ajouter avant "exit 0" dans le fichier /etc/rc.local la ligne suivant
```bash
screen -A -m -d -S lcd /opt/PvMonit/lcd/lcd-launch.sh
```
diff --git a/config-default.yaml b/config-default.yaml
index f837cfb..0ceb059 100644
--- a/config-default.yaml
+++ b/config-default.yaml
@@ -1,219 +1,220 @@
######################################################################
# PvMonit - By David Mercereau : http://david.mercereau.info/contact/
# Licence BEERWARE
# Version 1.0
######################################################################
##############################
#
# NE MODIFIER PAS CE FICHIER !
# copier config-default.yaml dans config.yaml et modifier config.yaml (supprimer tout ce qui ne vous intéresse pas de modifier)
#
##############################
# Niveau d'affichage des messages
printMessage: 0 # 0=0 5=debug
printMessageLogfile: false # path or fase
# URL data
urlDataXml: http://pvmonit.chezvous/data-xml.php # Utiliser un domaine qui pointe vers l'ip, not localhost or 127.0.0.1
tmpFileDataXml: /tmp/PvMonit_data-xml.php.tmp
dir:
bin: /opt/PvMonit/bin/
bin_enabled: /opt/PvMonit/bin-enabled/
lcd: /opt/PvMonit/lcd/
domo: /opt/PvMonit/domo/
data:
ppv_total: false # production total des régulateurs (utilisé si vous avez plusieurs régulateur)
conso_calc: false # Calculé avec : la puissance instantané (P du BMV) - ppv_total ppv_total at true for use this
cache:
dir: /tmp/PvMonit_cache # in tmpfs
file_prefix:
time: 60 # in second
# Methode de récupération des données VE DIRECT (par USB - vedirect OU serial par Arduino)
vedirect:
by: usb # usb OR arduino
usb:
# Binaire de vedirect.py USB
bin: /usr/bin/sudo /usr/bin/python /opt/PvMonit/bin/vedirect.py
arduino:
# Fichier de data YAML enregistré par le script vedirectOnArduinoRemote.py cohérence avec config-vedirectOnArduinoRemote.yaml
data_file: /tmp/PvMonit_getSerialArduino.data.yaml
data_file_expir: 300 # Expiration
serial:
port: /dev/ttyAMA0 # ttyAMA0 pour le serial via GPIO, ttyUSB0 pour le port USB...
timeout: 0
# Débit du serial 0 qui va vers l'Arduino (doit être cohérent entre les 2, diffère selon la distance de câble)
# Débit Longueur (m)
# 2400 60
# 4 800 30
# 9 600 15
# 19 200 7,6
# https://fr.wikipedia.org/wiki/RS-232#Limites
baudRate: 4800
whileSleep: 0.001
whileSleepAfterStop: 3
# donnée récolté (voir la doc victron sur le protocole VE.Direct)
data_ok:
mppt:
- CS
- PPV
- V
- ERR
- I
- VPV
- H19
- H20
- H21
- H22
- H23
bmv:
- V
- VS
- VM
- DM
- I
- T
- P
- CE
- SOC
- TTG
- AR
- H1
- H2
- H3
- H4
- H5
- H6
- H7
- H8
- H9
- H10
- H11
- H12
- H13
- h14
- H15
- H16
- H17
- H18
phoenix:
- P
- CS
- MODE
- AC_OUT_V
- AC_OUT_I
- WARN
# Numéro de série (champs SER#) en correspondance avec des nom buvables
deviceCorrespondance:
HQXXXXXXXX: MpttGarage
HQYYYYYYYY: MpttToit
# Plafont de consommation en W impossible à dépasser (techniquement, sinon c'est une erreur de sonde)
consoPlafond: 1500
# Tension standard du réseau (110V ou 230V)
tensionNorme: 230
### Export vers Emoncms
emoncms:
# Test la connexion internet
testInternetHost: emoncms.org
testInternetPort: 80
# emoncms URL du post.json & API key
urlInputJsonPost: https://emoncms.org/input/post.json
apiKey: XXXXXXXXXXXXXXXXXXXXXXXX
# Répertoire de collecte de données
dataCollecte: /tmp/PvMonit_collecteData
# Dossier ou ranger les erreurs
dataCollecteError: /tmp/PvMonit_collecteDataError
# Attente entre deux requête OK
sleepOk: 1
# Attente entre deux requête échoué
sleepNok: 3
# Fichier de lock pour éviter les doublons
lockFile: /tmp/PvMonit_sendToEmoncms.lock
### Page Web :
www:
# Délais de raffraichissement de la page (en seconde) 300000 = 5 minutes
refreshTime: 300000
# Max de la jauge voltage batterie (en V)
vbatMax: 30
# Max de la jauge puissance PV (en W)
PpvMax: 500 # max Jauge puissance PV (en W)
# Max de la jauge puissance PV total (si plusieurs régulateur) (en W)
PpvtMax: 500 # max Jauge puissance PV (en W)
dataPrimaire:
- V
- PPV
- ERR
- CS
- SOC
- AR
- P
- TTG
- MODE
- AC_OUT_V
- AC_OUT_I
- WARN
- PPVT
- CONSO
dataPrimaireSmallScreen:
- SOC
- P
- PPVT
menu:
-
+ checkUpdate: 43200 # false for disable, or seconds checks
# Ecran LCD (avec script PvMonit/lcd
lcd:
rafraichissement: 0.1 # en seconde pour les boutons
dataUpdate: 45 # en seconde pour le rafraichissement des données
onTimer: 60 # en seconde le temps que l'écran reste allumé si par défaut éteind
estCeLaNuitTimer: 600 # détection de la nuit tout les x secondes
dataPrint:
- SOC
- P
- PPVT
onAt: 8 # heure d'allumage du LCD
offAt: 21 # heure d'extinction du LCD
# Domotique (avec script PvMonit/domo)
domo:
dataCheckTime: 30 # Check du XML
dbFile: /tmp/PvMonit_domo.sqlite3
i2c:
adress: 0x04
device: 1
heartbeatTime: 4 # Fréquence du bâtement de coeur (en seconde)
fileExpir: 180 # Age en seconde après laquel le fichier n'est plus considéré comme ok
fileCheckError: 5 # nombre d'erreurs sur le xml
xmlDataExpir: 500 # Age en seconde avant expiration des données XML (et donc arrêt du heartbeat)
valueUse:
SOC: ^-?[0-9]+\.?[0-9]+?$|^[0-9]$
P: ^-?[0-9]+\.?[0-9]+?$|^[0-9]$
PPVT: ^-?[0-9]+\.?[0-9]+?$|^[0-9]$
CS: ^Off$|Fault$|Bulk|Faible|Abs|Float|On$
relay:
nb: 5 # 14 max !
dataFreq: 180 # Délais de récupération des données depuis l'arduino (en secondes)
scriptDir: /opt/PvMonit/domo/relay.script.d
scriptExecInterval: 180 # Interval d'execution des script de relais
relayCorrespondance: # A commencer par 0
0: Pompe de relevage
1: Box Internet
2: Téléphone
3: Disque dur externe
4: Chargeur outil électroportatif # Sonde courant !
#5: Chauffe eau
#6: Batterie de vélo életrique
#7: Surpresseur
diff --git a/domo/domo.py b/domo/domo.py
index eb9c1de..f8c6a21 100644
--- a/domo/domo.py
+++ b/domo/domo.py
@@ -1,335 +1,368 @@
import yaml
import time
from smbus2 import SMBus
import os
from lxml import etree
from urllib.request import urlopen
import wget
from past.builtins import execfile
import re
import time
import sqlite3
import sys
import json
## for debug :
import pprint
with open('../config-default.yaml') as f1:
config = yaml.load(f1, Loader=yaml.FullLoader)
with open('../config.yaml') as f2:
config_perso = yaml.load(f2, Loader=yaml.FullLoader)
def configGet(key1, key2=None, key3=None, key4=None):
if key4 != None:
try:
return config_perso[key1][key2][key3][key4]
except:
return config[key1][key2][key3][key4]
elif key3 != None:
try:
return config_perso[key1][key2][key3]
except:
return config[key1][key2][key3]
elif key2 != None:
try:
return config_perso[key1][key2]
except:
return config[key1][key2]
else:
try:
return config_perso[key1]
except:
return config[key1]
# Cherche a savoir si le MPPT est en Absorption ou en Float (en fin de charge)
def MpptAbsOrFlo(cs):
patternAbsFlo = re.compile(r"Absorption|Float")
if patternAbsFlo.match(cs):
return True;
else :
return False;
+def MpptFlo(cs):
+ patternAbsFlo = re.compile(r"Float")
+ if patternAbsFlo.match(cs):
+ return True;
+ else :
+ return False;
# Function for log
def logMsg(level, msg):
if level <= configGet('printMessage') :
print(time.strftime ('%m/%d/%Y %H:%M') ," - ",msg)
return -1
# i2c write
def writeNumber(value):
bus.write_byte(configGet('domo', 'i2c', 'adress'), value)
return -1
def download_data():
# téléchargement des données
logMsg(3, 'Download data')
with open(configGet('tmpFileDataXml'), 'wb') as tmpxml:
tmpxml.write(urlopen(configGet('urlDataXml')).read())
return time.time()
logMsg(1, 'Lancement du script domo.py')
heartGo=False
heartLastCheck=0
relayDataLastCheck=0
scriptExecLast=0
xmlLastCheck=0
xmlfileCheckError=0
xmlData = {}
xmlDataLastControleOk=0
xmlDataControle=False
dernierScriptJoue=-1
sortie=False
firstDataRece=True
bus=SMBus(configGet('domo', 'i2c', 'device'));
# BD
if not os.path.isfile(configGet('domo', 'dbFile')):
con = sqlite3.connect(configGet('domo', 'dbFile'))
db = con.cursor()
# Création de la base si elle n'existe pas
db.execute('''CREATE TABLE relay (id INTEGER PRIMARY KEY, relay_number INTEGER, info TEXT, valeur INTEFER, date INTEGER, event TEXT)''')
con.commit()
else:
con = sqlite3.connect(configGet('domo', 'dbFile'))
db = con.cursor()
def logInDb(relay, info, valeur, event):
db.execute("INSERT INTO relay VALUES (null,"+str(relay)+",'"+str(info)+"',"+str(valeur)+"," + str(int(time.time())) + ",'"+str(event)+"')")
con.commit()
def relayLastUp(relay):
req=db.execute('SELECT date FROM relay WHERE relay_number = '+str(relay)+' AND info = "E" AND (valeur = 2 OR valeur = 3) ORDER BY date DESC LIMIT 1')
try:
return req.fetchone()[0]
except:
return 0
def relayLastDown(relay):
req=db.execute('SELECT date FROM relay WHERE relay_number = '+str(relay)+' AND info = "E" AND (valeur = 0 OR valeur = 1) ORDER BY date DESC LIMIT 1')
try:
return req.fetchone()[0]
except:
return 0
# Est-ce que le relay c'est allumé puis est maintenant éteind aujourd'hui ? (dans les 12 heures)
def relayUpDownToday(relay):
- print('SELECT count(date) FROM relay WHERE relay_number = '+str(relay)+' AND info = "E" AND (valeur = 1 OR valeur = 2) AND date > ' + str(t-720) +' ORDER BY date DESC LIMIT 2')
req=db.execute('SELECT count(date) FROM relay WHERE relay_number = '+str(relay)+' AND info = "E" AND (valeur = 1 OR valeur = 2) AND date > ' + str(t-720) + ' ORDER BY date DESC LIMIT 2')
try:
if req.fetchone()[0] == 2:
return True
else:
return False
except:
return False
+# Est-ce que le relay c'est allumé aujourd'hui ? (dans les 12 heures)
+def relayUpToday(relay):
+ req=db.execute('SELECT count(date) FROM relay WHERE relay_number = '+str(relay)+' AND info = "E" AND (valeur = 2 OR valeur = 3) AND date > ' + str(t-720) + ' ORDER BY date DESC LIMIT 1')
+ try:
+ if req.fetchone()[0] == 1:
+ return True
+ else:
+ return False
+ except:
+ return False
+
+
+
+def timeUpMax(timeUp):
+ # Si le temps
+ if relayLastUp(relayId)+timeUp < t:
+ return True
+ else:
+ return False
+
+def timeUpMin(timeUp):
+ # Si le temps
+ if relayLastUp(relayId)+timeUp > t:
+ return True
+ else:
+ return False
# ~ @todo : Mode debug in write
# ~ #########################
# ~ # Paramètre lancement script
# ~ #########################
# ~ # HELP @todo à faire !!!
# ~ patternHelp = re.compile("help$")
# ~ if patternHelp.match(sys.argv[1]):
# ~ print("")
# ~ print("Usage: python3 "+sys.argv[0]+" debugScript RelayNumber ObjectDataFile")
# ~ print(" - RelayNumber : 0-9")
# ~ print(" - jsonDataFile : /tmp/Pvmonit_domo_debug.data (option, sinon c'est téléchargé en direct)")
# ~ print("")
# ~ print("Le fichier jsonDataFile contient par exemple : ")
# ~ print("{'CS': 'Bulk (en charge)', 'P': '-58', 'PPVT': '6', 'SOC': '95.6'}")
# ~ sys.exit(1)
# ~ # DEBUG
# ~ patternDebug = re.compile("debugScript")
# ~ if patternDebug.match(sys.argv[1]):
# ~ if len (sys.argv) != 3 :
# ~ print("Erreur, il manque des arguments pour le debug : ")
# ~ print("Usage: python3 "+sys.argv[0]+" debugScript RelayNumber ObjectDataFile")
# ~ print(" - RelayNumber : 0-9 (numéro du script contenu dans relay.script.d)")
# ~ print(" - jsonDataFile : /tmp/Pvmonit_domo_debug.data (option, sinon c'est téléchargé en direct)")
# ~ print("")
# ~ print("Le fichier jsonDataFile contient par exemple : ")
# ~ print("{'CS': 'Bulk (en charge)', 'P': '-58', 'PPVT': '6', 'SOC': '95.6'}")
# ~ sys.exit (1)
logMsg(3, "Début de la boucle")
while 1:
# XML data recup
t=int(time.time())
# S'il y a trop d'erreur :
if xmlfileCheckError >= configGet('domo', 'fileCheckError'):
logMsg(1, 'Trop d\'erreur, on patiente 10 secondes')
time.sleep(10)
xmlfileCheckError=0
if xmlLastCheck+configGet('domo', 'dataCheckTime') < t:
download_data()
if not os.path.isfile(configGet('tmpFileDataXml')):
logMsg(2, "Le fichier XML de donnée " + configGet('tmpFileDataXml') + " n'existe pas.")
xmlfileCheckError=xmlfileCheckError+1
elif os.path.getmtime(configGet('tmpFileDataXml'))+configGet('domo', 'fileExpir') < t :
logMsg(2, "Le fichier data est périmé !")
xmlfileCheckError=xmlfileCheckError+1
else:
logMsg(3, "Récupération des données XML (état de l'installation solaire)")
try:
# Tentative de lecture
tree = etree.parse(configGet('tmpFileDataXml'))
datacount=0
for datas in tree.xpath("/devices/device/datas/data"):
if datas.get("id") in configGet('domo', 'valueUse'):
datacount = datacount + 1
for data in datas.getchildren():
if data.tag == "value":
xmlData[datas.get("id")]=data.text
logMsg(5, pprint.pprint(xmlData))
# Test intégrité des données
controle=configGet('domo', 'valueUse')
xmlDataControle=True
for xmlDataVerif in xmlData:
pattern = re.compile(controle[xmlDataVerif])
if not pattern.match(xmlData[xmlDataVerif]):
logMsg(2, "Contrôle données erreur : " + xmlDataVerif + " = " + xmlData[xmlDataVerif])
xmlDataControle=False
except:
# Si ce n'est pas bon, c'est que les données ne sont pas bonnes ou incomplètes, on télécharge donc de nouveau le fichier XML
logMsg(1, "Erreur dans la lecture du XML la syntax n'est pas bonne ?")
xmlDataControle=False
if xmlDataControle == True:
xmlLastCheck=t
xmlDataLastControleOk=t
xmlfileCheckError=0
heartGo=True
else:
# Directement on ce met en mode "trop d'erreur" on patiente
xmlfileCheckError=configGet('domo', 'fileCheckError')
if xmlDataLastControleOk+configGet('domo', 'xmlDataExpir') < t:
logMsg(1, "Les données XML ont expirés, on stop le hearbeat")
heartGo=False
#########################
# Le heartbeat
#########################
if heartGo == True and heartLastCheck+configGet('domo', 'heartbeatTime') < t:
writeNumber(int(ord("H")))
logMsg(5, 'Heardbeat envoyé')
heartLastCheck=t
# Si les données sont bonnes :
if (xmlDataControle == True):
#########################
# Data Relay
#########################
if relayDataLastCheck+configGet('domo', 'relay', 'dataFreq') < t:
logMsg(4, 'On récupère les données des relay (via i2c arduino)')
# A FAIRE
# Simulation monsieur l'arbitre
#// Etat :
#// - 0 : off force
#// - 1 : off auto
#// - 2 : on auto
#// - 3 : on force
#// Mode
#// - 0 : Null
#// - 1 : Off
#// - 2 : Auto
#// - 3 : On
time.sleep(0.3)
# Requête i2c pour demande de data (état et mode des relay)
i2cResults = bus.read_i2c_block_data(configGet('domo', 'i2c', 'adress'), int(ord('D')), configGet('domo', 'relay', 'nb')*2+1)
# On conserve pour comparaison
try:
relayEtatOld=relayEtat
firstDataRece=False
except:
firstDataRece=True
# Remise à 0
relayEtat=[]
relayMod=[]
x=0
dataOrdre=1
logEtat=""
logMod=""
for i2cDatas in i2cResults:
# Si les données sont présentes
if i2cDatas != 255:
if i2cDatas == 29: # C'est le sépartateur : https://fr.wikibooks.org/wiki/Les_ASCII_de_0_%C3%A0_127/La_table_ASCII
dataOrdre=2
x=0
elif dataOrdre == 1:
relayEtat.insert(x,i2cDatas)
logEtat=logEtat+","+str(i2cDatas)
# Détection des changement pour les mettres en BD
if firstDataRece == False:
if i2cDatas != relayEtatOld[x]:
- testDoublon=db.execute('SELECT count(id) FROM relay WHERE relay_number = ' + str(x) + ' AND info = "E" AND valeur = '+str(i2cDatas))
+ print('un changement !!!')
+ print('SELECT count(id) FROM relay WHERE relay_number = ' + str(x) + ' AND info = "E" AND valeur = '+str(i2cDatas))
+ testDoublon=db.execute('SELECT count(id) FROM relay WHERE relay_number = ' + str(x) + ' AND info = "E" AND valeur = '+str(i2cDatas)+' AND date > ' + str(t-configGet('domo', 'relay', 'dataFreq')) )
if testDoublon.fetchone()[0] == 0:
+ print('enregistrement en BD')
logInDb(x, 'E', i2cDatas, '')
# Premier lancement, on enregistre les init en base
if firstDataRece == True:
logInDb(x, 'E', i2cDatas, 'Init')
else:
relayMod.insert(x,i2cDatas)
logMod=logMod+","+str(i2cDatas)
x=x+1
logMsg(3, "DATA reçu : Etat " + logEtat)
logMsg(3, "DATA reçu : Mod " + logMod)
relayDataLastCheck=t
#########################
# On joue les scripts
#########################
if scriptExecLast+configGet('domo', 'relay', 'scriptExecInterval') < t:
logMsg(4, 'On joue les script des relay en mode auto')
relayId=0
for mod in relayMod:
scriptFile=configGet('domo','relay', 'scriptDir') + "/" + str(relayId) + ".py"
if (relayMod[relayId] != 2):
logMsg(4, 'Le relay ' + str(relayId) + ' n\'est pas en mode automatique, mais en mode ' + str(relayMod[relayId]))
elif (relayEtat[relayId] == 0):
logMsg(4, 'Les relay sont encore en état 0, les mods sont donc juste changé et les relay n\'on pas eu le temps de changé d\'état, on patiente')
elif not os.path.isfile(scriptFile):
logMsg(4, 'Pas de script ' + scriptFile)
else:
# On joue les script 1 par 1, on attend pour jouer le suivant
if (relayId > dernierScriptJoue):
dernierScriptJoue=relayId
logMsg(3, 'Lecture du script ' + scriptFile)
returnEtat=None
returnLog=None
execfile(scriptFile)
if returnLog != None:
- logMsg(1, '['+str(relayId)+']' + returnLog)
+ logMsg(1, '['+str(relayId)+'] ' + returnLog)
if returnEtat != None and returnEtat != relayEtat[relayId]:
logMsg(1, 'Un changement d\'état vers ' + str(returnEtat) + ' de est demandé pour le relay ' + str(relayId))
data=[relayId,returnEtat]
bus.write_i2c_block_data(configGet('domo', 'i2c', 'adress'), int(ord('O')), data)
time.sleep(0.2)
sortie=True
logInDb(relayId, 'E', returnEtat, returnLog)
-
else:
logMsg(4, 'Pas de changement d\'état demandé pour le relay ' + str(relayId))
# Remise à 0 de la position de lecture des scripts
if (relayId >= len(relayMod)-1):
dernierScriptJoue=-1
relayId=relayId+1
# Sortie de la boucle demandé
if (sortie == True):
sortie=False
break
scriptExecLast=t
# Pour être gentil avec le système
time.sleep(0.05)
con.close()
diff --git a/domo/relay.script.d/0.py b/domo/relay.script.d/0.py
index 9a57e29..42d8dad 100644
--- a/domo/relay.script.d/0.py
+++ b/domo/relay.script.d/0.py
@@ -1,57 +1,50 @@
####################################
# Ce script est un exemple pour vous
# il est a adapté a vos besoin
####################################
# Script pour ma box internet
# config
pingHost=["192.168.1.10","192.168.1.12"]
def checkIfComputerIsUp(pingHost) :
onlineHost=0
pingBin="ping -i 0.2 -W 1 -c1 "
if os.path.isfile('/usr/bin/fping'):
pingBin='/usr/bin/fping -c1 -t500 '
logMsg(5, "Ping bin utilisé : " + pingBin)
for host in pingHost:
logMsg(5, pingBin + host + '>/dev/null 2>/dev/null')
response = os.system(pingBin + host + '>/dev/null 2>/dev/null')
#and then check the response...
if response == 0:
onlineHost=onlineHost+1
logMsg(5, 'onlineHost ' + str(onlineHost))
return onlineHost
-# ~ # Si il est éteind, faut-il l'allumer ?
-# ~ if relayEtat[relayId] == 1:
- # ~ # Si le régulateur dit que c'est bientôt la fin de charge et qu'il est plus de 11h c'est qu'il va faire beau !
- # ~ if MpptAbsOrFlo(xmlData['CS']) or (float(xmlData['SOC']) > 93 and int(time.strftime ('%H')) > 11):
- # ~ returnEtat=2
-# ~ # Si il est allumé, faut-il l'éteindre ?
-# ~ elif relayEtat[relayId] == 2:
- # ~ nbComputerUp=checkIfComputerIsUp(pingHost)
- # ~ # S'il n'y a plus d'ordinateur d'allumé et que les batterie sont sous les 95% ou qu'il est après 17h on éteind
- # ~ if (nbComputerUp == 0 and float(xmlData['SOC']) < 95) or (nbComputerUp == 0 and int(time.strftime ('%H')) >= 17) :
- # ~ returnEtat=1
+timeUp=600
# Si il est éteind, faut-il l'allumer ?
if relayEtat[relayId] == 1:
# Si le régulateur dit que c'est bientôt la fin de charge et qu'il est plus de 11h c'est qu'il va faire beau !
if MpptAbsOrFlo(xmlData['CS']):
- logMsg(2, '[0] UP Le régulateur est en mode CS')
+ returnLog='UP Le régulateur est en mode abs ou float'
returnEtat=2
if float(xmlData['SOC']) > 93 and int(time.strftime ('%H')) > 11 and int(time.strftime ('%H')) < 17:
- logMsg(2, '[0] UP La batterie est chargé à plus de 93% et qu\'il est entre 11 et 17h')
+ returnLog='UP La batterie est chargé à plus de 93% et qu\'il est entre 11 et 17h'
returnEtat=2
# Si il est allumé, faut-il l'éteindre ?
elif relayEtat[relayId] == 2:
nbComputerUp=checkIfComputerIsUp(pingHost)
# S'il n'y a plus d'ordinateur d'allumé et que les batterie sont sous les 95% ou qu'il est après 17h on éteind
if (nbComputerUp == 0 and float(xmlData['SOC']) <= 93):
- logMsg(2, '[0] DOWN pas d`ordinateur connecté et les batterie sous 93%')
+ returnLog='DOWN pas d`ordinateur connecté et les batterie sous 93%'
returnEtat=1
if (nbComputerUp == 0 and int(time.strftime ('%H')) >= 17) :
- logMsg(2, '[0] DOWN pas d`ordinateur connecté et il est plus de 17h')
+ returnLog='DOWN pas d`ordinateur connecté et il est plus de 17h'
returnEtat=1
+ if timeUpMin(timeUp):
+ returnLog='On maintient allumé, '
+ returnEtat=2
diff --git a/domo/relay.script.d/1.py b/domo/relay.script.d/1.py
index f1cb209..4e9420e 100644
--- a/domo/relay.script.d/1.py
+++ b/domo/relay.script.d/1.py
@@ -1,31 +1,18 @@
-# ~ # Script pour mon téléphone fixe
-
-# ~ # Si il est éteind, faut-il l'allumer ?
-# ~ if relayEtat[relayId] == 1:
- # ~ # Si la box est allumé automatiquement ou de force
- # ~ if relayEtat[0] >= 2:
- # ~ # Si le régulateur dit que c'est bientôt la fin de charge et qu'il est plus de 11h c'est qu'il va faire beau !
- # ~ if MpptAbsOrFlo(xmlData['CS']) or (float(xmlData['SOC']) > 95 and int(time.strftime ('%H')) > 11):
- # ~ returnEtat=2
-# ~ # Si il est allumé, faut-il l'éteindre ?
-# ~ elif relayEtat[relayId] == 2:
- # ~ # Si la box est éteinte automatiquement ou de force
- # ~ # Si les batterie sont sous les 95% ou qu'il est après 21h on éteind
- # ~ if relayEtat[0] <= 1 or float(xmlData['SOC']) <= 95 or int(time.strftime ('%H')) >= 21:
- # ~ returnEtat=1
-
# Script pour mon téléphone fixe
# Par défaut on le laisse éteind
returnEtat=1
+timeUp=1800
# Si la box est allumé automatiquement ou de force
if relayEtat[0] >= 2:
# Si le régulateur dit que c'est bientôt la fin de charge et qu'il est plus de 11h c'est qu'il va faire beau !
- if MpptAbsOrFlo(xmlData['CS']):
- returnLog='[UP Le régulateur est en mode CS'
+ if MpptFlo(xmlData['CS']):
+ returnLog='UP Le régulateur est en mode float'
returnEtat=2
if float(xmlData['SOC']) > 95 and int(time.strftime ('%H')) > 11 and int(time.strftime ('%H')) < 19:
returnLog='UP La batterie est chargé à plus de 95% et il est enre 11h et 19h'
returnEtat=2
-
+ if timeUpMin(timeUp):
+ returnLog='On maintient allumé, '
+ returnEtat=2
diff --git a/domo/relay.script.d/2.py b/domo/relay.script.d/2.py
index 47258cd..70fccb8 100644
--- a/domo/relay.script.d/2.py
+++ b/domo/relay.script.d/2.py
@@ -1,6 +1,16 @@
# Pompe de rellevage
# Par défaut on laisse éteind
returnEtat=1
-if
+# Temps d'allumage
+timeUp=300
+
+# Si elle a démarré aujourd'hui et que le temps d'allumage maxium est passé alors on le laisse à down
+if relayUpToday(relayId) and timeUpMax(timeUp):
+ returnLog='DOWN, le temps d allumage est passé'
+ returnEtat=1
+# Sinon on le lance si la batterie est à 100%
+elif float(xmlData['SOC']) > 100:
+ returnLog='UP La batterie est chargé à 100%'
+ returnEtat=2
diff --git a/domo/relay.script.d/3.py b/domo/relay.script.d/3.py
index b58cf7b..6e23157 100644
--- a/domo/relay.script.d/3.py
+++ b/domo/relay.script.d/3.py
@@ -1,15 +1,17 @@
# Script pour la rechage makita
-# Temps sans bagotter (en secondes)
-upMini=600
+# Par défaut on laisse éteind
+returnEtat=1
-# Après la pompe de relevage (2)
-if relayUpDownToday(2):
+# Temps d'allumage
+timeUp=3600
+
+# Si démarré aujourd'hui et que le temps d'allumage maxium est passé alors on le laisse à down
+if relayUpToday(relayId) and timeUpMax(timeUp):
+ returnLog='DOWN, le temps d allumage est passé'
returnEtat=1
- if float(xmlData['SOC']) > 98:
+# Sinon on le lance si la batterie est à 100% & que la pome de relevage c'est lancé aujourd'hui
+elif float(xmlData['SOC']) > 100 and relayUpDownToday(2):
returnLog='UP La batterie est chargé à 100%'
returnEtat=2
- # Minimum de temps sur l'état haut
- if relayEtat[relayId] == 2 && relayLastUp(relayId) < t+upMini:
- returnLog='[1] UP La batterie est chargé à 100%'
- returnEtat=2
+
diff --git a/domo/relay.script.d/7.py b/domo/relay.script.d/7.py
index e464c3b..c68aaba 100644
--- a/domo/relay.script.d/7.py
+++ b/domo/relay.script.d/7.py
@@ -1,13 +1,13 @@
# Test DD externe par exemple
# Si il est éteind, faut-il l'allumer ?
if relayEtat[relayId] == 1:
if os.path.isfile('/tmp/domo2up'):
- returnLog='Présence du fichier /tmp/domo2up'
+ returnLog='UP Présence du fichier /tmp/domo2up'
returnEtat=2
# Si il est allumé, faut-il l'éteindre ?
elif relayEtat[relayId] == 2:
if not os.path.isfile('/tmp/domo2up'):
- returnLog='Le fichier n est /tmp/domo2up n existe plus'
+ returnLog='DOWN Le fichier n est /tmp/domo2up n existe plus'
returnEtat=1
diff --git a/function.php b/function.php
index 07b7ec7..a0aaeaf 100644
--- a/function.php
+++ b/function.php
@@ -1,677 +1,681 @@
$perso1) {
if ($key1 == 'deviceCorrespondance') {
$config[$key1]=$perso1;
} elseif (is_array($perso1)) {
foreach($perso1 as $key2=>$perso2) {
if (is_array($perso2)) {
foreach($perso2 as $key3=>$perso3) {
if (isset($config[$key1][$key2][$key3])) {
$config[$key1][$key2][$key3]=$perso3;
}
}
}elseif (isset($config[$key1][$key2])) {
$config[$key1][$key2]=$perso2;
}
}
} elseif (isset($config[$key1])) {
$config[$key1]=$perso1;
}
}
return $config;
}
# Victron : détermine le type d'appareil
# Source doc Victron "VE.Direct Protocol"
function ve_type($ve_pid) {
if (substr($ve_pid, 0, -1) == '0x20') {
$ve_type_retour='BMV';
} else if (substr($ve_pid, 0, -2) == '0xA0' || $ve_pid == '0x300') {
$ve_type_retour='MPTT';
} else if (substr($ve_pid, 0, -2) == '0xA2') {
$ve_type_retour='PhoenixInverter';
} else {
$ve_type_retour='Inconnu';
}
return $ve_type_retour;
}
# Victron : détermine le modèle de l'appareil
# Source doc Victron "VE.Direct Protocol"
function ve_modele($ve_pid) {
switch ($ve_pid) {
case '0x203': $ve_modele_retour='BMV-700'; break;
case '0x204': $ve_modele_retour='BMV-702'; break;
case '0x205': $ve_modele_retour='BMV-700H'; break;
case '0xA04C': $ve_modele_retour='BlueSolar MPPT 75/10'; break;
case '0x300': $ve_modele_retour='BlueSolar MPPT 70/15'; break;
case '0xA042': $ve_modele_retour='BlueSolar MPPT 75/15'; break;
case '0xA043': $ve_modele_retour='BlueSolar MPPT 100/15'; break;
case '0xA044': $ve_modele_retour='BlueSolar MPPT 100/30 rev1'; break;
case '0xA04A': $ve_modele_retour='BlueSolar MPPT 100/30 rev2'; break;
case '0xA041': $ve_modele_retour='BlueSolar MPPT 150/35 rev1'; break;
case '0xA04B': $ve_modele_retour='BlueSolar MPPT 150/35 rev2'; break;
case '0xA04D': $ve_modele_retour='BlueSolar MPPT 150/45'; break;
case '0xA040': $ve_modele_retour='BlueSolar MPPT 75/50'; break;
case '0xA045': $ve_modele_retour='BlueSolar MPPT 100/50 rev1'; break;
case '0xA049': $ve_modele_retour='BlueSolar MPPT 100/50 rev2'; break;
case '0xA04E': $ve_modele_retour='BlueSolar MPPT 150/60'; break;
case '0xA046': $ve_modele_retour='BlueSolar MPPT 150/70'; break;
case '0xA04F': $ve_modele_retour='BlueSolar MPPT 150/85'; break;
case '0xA047': $ve_modele_retour='BlueSolar MPPT 150/100'; break;
case '0xA051': $ve_modele_retour='SmartSolar MPPT 150/100'; break;
case '0xA050': $ve_modele_retour='SmartSolar MPPT 250/100'; break;
case '0xA201': $ve_modele_retour='Phoenix Inverter 12V 250VA 230V'; break;
case '0xA202': $ve_modele_retour='Phoenix Inverter 24V 250VA 230V'; break;
case '0xA204': $ve_modele_retour='Phoenix Inverter 48V 250VA 230V'; break;
case '0xA211': $ve_modele_retour='Phoenix Inverter 12V 375VA 230V'; break;
case '0xA212': $ve_modele_retour='Phoenix Inverter 24V 375VA 230V'; break;
case '0xA214': $ve_modele_retour='Phoenix Inverter 48V 375VA 230V'; break;
case '0xA221': $ve_modele_retour='Phoenix Inverter 12V 500VA 230V'; break;
case '0xA222': $ve_modele_retour='Phoenix Inverter 24V 500VA 230V'; break;
case '0xA224': $ve_modele_retour='Phoenix Inverter 48V 500VA 230V'; break;
default; $ve_modele_retour = 'Inconnu'; break;
}
return $ve_modele_retour;
}
# Victron : détermine plein de trucs en fonction du label
# Source doc Victron "VE.Direct Protocol"
function ve_label2($label, $valeur) {
global $config;
$veData['label']=$label;
$veData['desc']=$label;
$veData['value']=$valeur;
$veData['units']='';
$veData['screen']=0;
$veData['smallScreen']=0;
if (in_array($label, $config['www']['dataPrimaire'])) {
$veData['screen']=1;
}
if (in_array($label, $config['www']['dataPrimaireSmallScreen'])) {
$veData['smallScreen']=1;
}
switch ($label) {
case 'V':
$veData['value']=round($valeur*0.001, 2);
$veData['desc']='Tension de la batterie';
$veData['units']='V';
break;
case 'I':
$veData['value']=$valeur*0.001;
$veData['desc']='Courant de la batterie';
$veData['units']='A';
break;
case 'PPV':
$veData['desc']='Production des panneaux';
$veData['descShort']='PV';
$veData['units']='W';
break;
case 'ERR':
$veData['desc']='Présence d\'erreur';
if ($valeur == 0) {
$veData['value']='Aucune';
} else {
switch ($veData['value']) {
case 2: $veData['value'] = 'Battery voltage too high'; break;
case 17: $veData['value'] = 'Charger temperature too high'; break;
case 18: $veData['value'] = 'Charger over current'; break;
case 19: $veData['value'] = 'Charger current reversed'; break;
case 20: $veData['value'] = 'Bulk time limit exceeded'; break;
case 21: $veData['value'] = 'Current sensor issue (sensor bias/sensor broken)'; break;
case 26: $veData['value'] = 'Terminals overheated'; break;
case 33: $veData['value'] = 'Input voltage too high (solar panel)'; break;
case 34: $veData['value'] = 'Input current too high (solar panel)'; break;
case 38: $veData['value'] = 'Input shutdown (due to excessive battery voltage)'; break;
case 116: $veData['value'] = 'Factory calibration data lost'; break;
case 117: $veData['value'] = 'Invalid/incompatible firmware'; break;
case 119: $veData['value'] = 'User settings invalid'; break;
default: $veData['value'] = $dataSplit[1]; break;
}
}
break;
case 'VPV':
$veData['desc']='Voltage des panneaux';
$veData['units']='mV';
break;
case 'H19':
$veData['value']=$valeur*0.01;
$veData['desc']='Le rendement total';
$veData['units']='kWh';
break;
case 'H20':
$veData['value']=$valeur*0.01;
$veData['desc']='Rendement aujourd\'hui';
$veData['units']='kWh';
break;
case 'H21':
$veData['desc']='Puissance maximum ce jour';
$veData['units']='W';
break;
case 'H22':
$veData['value']=$valeur*0.01;
$veData['desc']='Rendemain hier';
$veData['units']='kWh';
break;
case 'H23':
$veData['desc']='Puissance maximum hier';
$veData['units']='W';
break;
case 'AR':
$veData['desc']='Raison de l\'alarme';
switch ($veData['value']) {
case 0: $veData['value']= 'Aucune'; break;
case 1: $veData['value']= 'Low Voltage'; break;
case 2: $veData['value']= 'High Voltage'; break;
case 4: $veData['value']= 'Low SOC'; break;
case 8: $veData['value']= 'Low Starter Voltage'; break;
case 16: $veData['value']= 'High Starter Voltage'; break;
case 32: $veData['value']= 'Low Temperature'; break;
case 64: $veData['value']= 'High Temperature'; break;
case 128: $veData['value']= 'Mid Voltage'; break;
case 256: $veData['value']= 'Overload'; break;
case 512: $veData['value']= 'DC-ripple'; break;
case 1024: $veData['value']= 'Low V AC out'; break;
case 2048: $veData['value']= 'High V AC out'; break;
}
break;
case 'CS':
$veData['desc']='Status de charge';
switch ($veData['value']) {
case 0: $veData['value']= 'Off'; break;
case 1: $veData['value']= 'Faible puissance'; break;
case 2: $veData['value']= 'Fault'; break;
case 3: $veData['value']= 'Bulk (en charge)'; break;
case 4: $veData['value']= 'Absorption'; break;
case 5: $veData['value']= 'Float (maintient la charge pleine)'; break;
case 9: $veData['value']= 'On'; break;
}
break;
case 'P':
$veData['desc']='Puissance instantané';
$veData['units']='W';
break;
case 'T':
$veData['desc']='Température de la batterie';
$veData['units']='°C';
break;
case 'VM':
$veData['desc']='Mid-point voltage of the battery bank';
$veData['value']=$valeur*0.001;
$veData['units']='V';
break;
case 'DM':
$veData['desc']='Mid-point deviation of the battery bank';
$veData['units']='%';
break;
case 'H17':
$veData['desc']='Quantité d\'énergie déchargée';
$veData['value']=$valeur*0.01;
$veData['units']='kWh';
break;
case 'H18':
$veData['desc']='Quantité d\'énergie chargée';
$veData['value']=$valeur*0.01;
$veData['units']='kWh';
break;
case 'H13':
$veData['desc']='Number of low auxiliary voltage alarms';
break;
case 'H14':
$veData['desc']='Number of high auxiliary voltage alarms';
break;
case 'VS':
$veData['desc']='Auxiliary (starter) voltage';
$veData['value']=$valeur*0.001;
$veData['units']='V';
break;
case 'CE':
$veData['desc']='Ampères heures consommées';
$veData['value']=$valeur*0.001;
$veData['units']='Ah';
break;
case 'SOC':
$veData['desc']='État de charge';
$veData['value']=$valeur/10;
$veData['units']='%';
break;
case 'TTG':
if ($veData['value'] == '-1') {
$veData['value'] = '∞';
} else {
$total=$veData['value']*60;
$jours=floor($total/86400);
$reste=$total%86400;
$heures=floor($reste/3600);
$reste=$reste%3600;
$minutes=floor($reste/60);
$secondes=$reste%60;
if ($veData['value'] > 1440) {
$veData['value'] = $jours . 'j '. $heures. 'h ' . $minutes .'m';
} else {
$veData['value'] = '.'.$heures. 'h ' . $minutes .'m';
}
}
$veData['desc']='Temps restant';
break;
case 'Alarm':
$veData['desc']='Condition d\'alarme active';
break;
case 'H1':
$veData['desc']='Profondeur de la décharge la plus profonde';
$veData['value']=$valeur*0.001;
$veData['units']='Ah';
break;
case 'H2':
$veData['desc']='Profondeur de la dernière décharge';
$veData['value']=$valeur*0.001;
$veData['units']='Ah';
break;
case 'H3':
$veData['desc']='Profondeur de la décharge moyenne';
$veData['value']=$valeur*0.001;
$veData['units']='Ah';
break;
case 'H4':
$veData['desc']='Nombre de cycles de charge';
break;
case 'H5':
$veData['desc']='Nombre de cycles de décharge';
break;
case 'H6':
$veData['desc']='Cumulative Amp Hours drawn';
$veData['value']=$valeur*0.001;
$veData['units']='Ah';
break;
case 'H7':
$veData['desc']='Tension minimale batterie';
$veData['value']=$valeur*0.001;
$veData['units']='V';
break;
case 'H8':
$veData['desc']='Tension maximale de la batterie';
$veData['value']=$valeur*0.001;
$veData['units']='V';
break;
case 'H9':
$veData['desc']='Nombre de secondes depuis la dernière charge complète';
$veData['units']='s';
break;
case 'H10':
$veData['desc']='Nombre de synchronisations automatiques';
break;
case 'H11':
$veData['desc']='Nombre d\'alarmes de tension faible';
break;
case 'H12':
$veData['desc']='Nombre d\'alarmes de tension élevée';
break;
case 'H13':
$veData['desc']='Minimum auxiliary (battery) voltage';
$veData['value']=$valeur*0.001;
$veData['units']='V';
break;
case 'H13':
$veData['desc']='Maximum auxiliary (battery) voltage';
$veData['value']=$valeur*0.001;
$veData['units']='V';
break;
case 'MODE':
$veData['desc']='Device mode';
switch ($veData['value']) {
case 2: $veData['value']= 'Inverter'; break;
case 4: $veData['value']= 'Off'; break;
case 5: $veData['value']= 'Eco'; break;
}
break;
case 'AC_OUT_V':
$veData['value']=$valeur*0.01;
$veData['desc']='AC output voltage';
$veData['units']='V';
break;
case 'AC_OUT_I':
$veData['desc']='AC output current';
$veData['value']=$valeur*0.1;
$veData['units']='A';
break;
case 'WARN':
$veData['desc']='Warning reason';
break;
}
return $veData;
}
function ve_nom($ve_serial) {
global $config;
$ve_nom=$ve_serial;
foreach ($config['deviceCorrespondance'] as $serialName => $nom) {
if ($ve_serial == $serialName) {
$ve_nom=$nom;
}
}
return $ve_nom;
}
# Fonction vedirect MPTT / BMV
function vedirect_scan() {
global $config;
trucAdir(4, 'Recherche de périphérique vedirect');
$idDevice=0;
foreach (scandir('/dev') as $unDev) {
if (substr($unDev, 0, 6) == 'ttyUSB') {
trucAdir(4, 'Un périphérique TTY à été trouvé : '.$unDev);
unset($vedirect_sortie);
unset($vedirect_retour);
exec($config['vedirect']['usb']['bin'].' /dev/'.$unDev, $vedirect_sortie, $vedirect_retour);
if ($vedirect_retour != 0){
trucAdir(1, 'Erreur à l\'exécution du script '.VEDIRECT_BIN.' sur le '.$unDev);
} else {
// Pour gérer le BMV-600
$BMV600=false;
$ve_nom=null;
$ve_type='Inconnu';
$ve_modele='Inconnu';
$ve_type='Inconnu';
foreach ($vedirect_sortie as $vedirect_ligne) {
$vedirect_data = explode(':', $vedirect_ligne);
switch ($vedirect_data[0]) {
case 'PID':
$ve_type=ve_type($vedirect_data[1]);
$ve_modele=ve_modele($vedirect_data[1]);
break;
case 'SER#':
$ve_serial=$vedirect_data[1];
$ve_nom=ve_nom($vedirect_data[1]);
break;
case 'BMV':
$ve_type='BMV';
$ve_nom=$vedirect_data[1];
break;
}
}
trucAdir(3, 'C\'est un '.$ve_type.', modèle "'.$ve_modele.'" du nom de '.$ve_nom);
$vedirect_data_formate='';
foreach ($vedirect_sortie as $vedirect_ligne) {
$vedirect_data = explode(':', $vedirect_ligne);
switch ($ve_type) {
case 'MPTT':
if (in_array($vedirect_data[0], $config['vedirect']['data_ok']['mppt'])) {
# éviter les doublons
if (!stristr($vedirect_data_formate, "$key:$value")) {
trucAdir(5, 'Valeur trouvé : '.$vedirect_data[0].':'.$vedirect_data[1]);
if ($vedirect_data_formate != '') {
$vedirect_data_formate = $vedirect_data_formate.',';
}
$vedirect_data_formate = $vedirect_data_formate.$vedirect_data[0].':'.$vedirect_data[1];
} else {
trucAdir(5, 'Doublon, on passe');
}
}
break;
case 'BMV':
if (in_array($vedirect_data[0], $config['vedirect']['data_ok']['bmv'])) {
if ($vedirect_data_formate != '') {
$vedirect_data_formate = $vedirect_data_formate.',';
}
$vedirect_data_formate = $vedirect_data_formate.$vedirect_data[0].':'.$vedirect_data[1];
}
break;
case 'PhoenixInverter':
if (in_array($key, $config['vedirect']['data_ok']['phoenix'])) {
if ($vedirect_data_formate != '') {
$vedirect_data_formate = $vedirect_data_formate.',';
}
$vedirect_data_formate = $vedirect_data_formate.$key.':'.$value;
}
break;
default:
if ($vedirect_data_formate != '') {
$vedirect_data_formate = $vedirect_data_formate.',';
}
$vedirect_data_formate = $vedirect_data_formate.$key.':'.$value;
}
}
trucAdir(3, 'Les données sont formatées comme ceci : '.$vedirect_data_formate );
$vedirect_scan_return[$idDevice]['nom']=$ve_nom;
$vedirect_scan_return[$idDevice]['type']=$ve_type;
$vedirect_scan_return[$idDevice]['serial']=$ve_serial;
$vedirect_scan_return[$idDevice]['modele']=$ve_modele;
$vedirect_scan_return[$idDevice]['data']=$vedirect_data_formate;
$idDevice++;
}
}
}
return $vedirect_scan_return;
}
function vedirect_parse_arduino($data) {
global $config;
// Pour gérer le BMV-600
$BMV600=false;
$ve_nom=null;
$ve_type='Inconnu';
$ve_modele='Inconnu';
$ve_serial='Inconnu';
foreach ($data as $key => $value) {
switch ($key) {
case 'PID':
$ve_type=ve_type($value);
$ve_modele=ve_modele($value);
break;
case 'SER#':
$ve_serial=$value;
$ve_nom=ve_nom($value);
break;
case 'BMV':
$ve_type='BMV';
$ve_nom=$value;
break;
}
}
trucAdir(3, 'C\'est un '.$ve_type.', modèle "'.$ve_modele.'" du nom de '.$ve_nom);
$vedirect_data_formate='';
krsort($data);
foreach ($data as $key => $value) {
switch ($ve_type) {
case 'MPTT':
if (in_array($key, $config['vedirect']['data_ok']['mppt'])) {
# éviter les doublons
if (!stristr($vedirect_data_formate, "$key:$value")) {
trucAdir(5, 'Valeur trouvé : '.$key.':'.$value);
if ($vedirect_data_formate != '') {
$vedirect_data_formate = $vedirect_data_formate.',';
}
$vedirect_data_formate = $vedirect_data_formate.$key.':'.$value;
} else {
trucAdir(5, 'Doublon, on passe');
}
}
break;
case 'BMV':
if (in_array($key, $config['vedirect']['data_ok']['bmv'])) {
if ($vedirect_data_formate != '') {
$vedirect_data_formate = $vedirect_data_formate.',';
}
$vedirect_data_formate = $vedirect_data_formate.$key.':'.$value;
}
break;
case 'PhoenixInverter':
if (in_array($key, $config['vedirect']['data_ok']['phoenix'])) {
if ($vedirect_data_formate != '') {
$vedirect_data_formate = $vedirect_data_formate.',';
}
$vedirect_data_formate = $vedirect_data_formate.$key.':'.$value;
}
break;
default:
if ($vedirect_data_formate != '') {
$vedirect_data_formate = $vedirect_data_formate.',';
}
$vedirect_data_formate = $vedirect_data_formate.$key.':'.$value;
}
}
trucAdir(3, 'Les données sont formatées comme ceci : '.$vedirect_data_formate );
$vedirect_scan_return['nom']=$ve_nom;
$vedirect_scan_return['type']=$ve_type;
$vedirect_scan_return['serial']=$ve_serial;
$vedirect_scan_return['modele']=$ve_modele;
$vedirect_scan_return['data']=$vedirect_data_formate;
return $vedirect_scan_return;
}
# Fonction de debug
function trucAdir($niveau, $msg) {
global $config;
if ($config['printMessage'] >= $niveau) {
if (isset($_SERVER['SERVER_NAME'])) {
echo '';
} else {
echo date('c') . ' - ' . $msg."\n";
}
}
if ($config['printMessageLogfile'] != false) {
if (! is_file($config['printMessageLogfile'])) {
touch($config['printMessageLogfile']);
if (substr(sprintf('%o', fileperms($config['printMessageLogfile'])), -3) != '777') {
chmod($config['printMessageLogfile'], 0777);
}
}
file_put_contents($config['printMessageLogfile'], date('c') . ' - ' . $_SERVER['SCRIPT_NAME']. ' - ' . $msg . "\n", FILE_APPEND);
}
}
# Récupérer les informations de la sonde de température
function Temperature_USB($TEMPERV14_BIN) {
global $config;
# Exécussion du programme pour récupérer les inforamtions de la sonde de température
exec($TEMPERV14_BIN, $temperv14_sortie, $temperv14_retour);
if ($temperv14_retour != 0){
trucAdir(3, 'La sonde de température n\'est probablement pas connecté.');
trucAdir(5, 'Erreur '.$temperv14_retour.' à l\'exécussion du programme .'.$TEMPERV14_BIN);
$temperature_retour='NODATA';
} else {
trucAdir(4, 'La sonde de température indique '.$temperv14_sortie[0].'°C, il y aura peut être correction.');
$temperature_retour=$temperv14_sortie[0];
}
return $temperature_retour;
}
function Amp_USB($bin) {
global $config;
$consommation_retour='NODATA';
for ($i = 1; $i <= 3; $i++) {
trucAdir(3, 'Tentative '.$i.' de récupération de la sonde ');
exec($bin.' | sed "s/A//" 2>/dev/null', $exec_consommation_sortie, $exec_consommation_retour);
if ($exec_consommation_retour != 0){
trucAdir(3, 'L\'amphèrmètre n\'est probablement pas connecté.');
trucAdir(5, 'Erreur '.$exec_consommation_retour.' avec pour sortie .'.$exec_consommation_sortie);
} else {
if ($exec_consommation_sortie[0] != '') {
trucAdir(3, 'Trouvé à la tentative '.$i.' : la La consommation trouvé est '.$exec_consommation_sortie[0].'A');
$re = '/^[0-9][0-9]+.[0-9]$/';
if (!preg_match_all($re, $exec_consommation_sortie[0])) {
trucAdir(5, 'La vérification par expression régulière à échoué ('.$re.')');
} else {
$conso_en_w=$exec_consommation_sortie[0]*230;
trucAdir(1, 'La consommation est de '.$exec_consommation_sortie[0].'A soit '.$conso_en_w.'W');
if ($conso_en_w > $config['consoPlafond']) {
trucAdir(1, 'C`est certainement une erreur, le plafond possible est atteind');
} else {
$consommation_retour=$exec_consommation_sortie[0];
}
}
break;
} else {
trucAdir(5, 'Echec à la tentative '.$i.' : la La consommation trouvé est null');
sleep(1);
}
}
}
return $consommation_retour;
}
// Class source : http://abhinavsingh.com/how-to-use-locks-in-php-cron-jobs-to-avoid-cron-overlaps/
class cronHelper {
private static $pid;
function __construct() {}
function __clone() {}
private static function isrunning() {
$pids = explode(PHP_EOL, `ps -e | awk '{print $1}'`);
if(in_array(self::$pid, $pids))
return TRUE;
return FALSE;
}
public static function lock() {
global $config;
global $argv;
$lock_file = $config['emoncms']['lockFile'];
if(file_exists($lock_file)) {
//return FALSE;
// Is running?
self::$pid = file_get_contents($lock_file);
if(self::isrunning()) {
error_log("==".self::$pid."== Already in progress...");
return FALSE;
}
else {
error_log("==".self::$pid."== Previous job died abruptly...");
}
}
self::$pid = getmypid();
file_put_contents($lock_file, self::$pid);
//error_log("==".self::$pid."== Lock acquired, processing the job...");
return self::$pid;
}
public static function unlock() {
global $argv;
global $config;
$lock_file = $config['emoncms']['lockFile'];
if(file_exists($lock_file))
unlink($lock_file);
//error_log("==".self::$pid."== Releasing lock...");
return TRUE;
}
}
// Check cache expire
function checkCacheTime($file) {
global $config;
if (!is_dir($config['cache']['dir'])) {
mkdir($config['cache']['dir'], 0777);
chmod($config['cache']['dir'], 0777);
}
if (!is_file($file)) {
return false;
} else if (filemtime($file)+$config['cache']['time'] < time()) {
return false;
} else if (isset($_GET['nocache'])) {
return false;
} else {
return true;
}
}
+
?>
diff --git a/www/css/style.css b/www/css/style.css
index 4cc766e..f6a6f45 100644
--- a/www/css/style.css
+++ b/www/css/style.css
@@ -1,217 +1,221 @@
/* Generated by http://www.cssportal.com */
@import url("reset.css");
body {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 13px;
color:#333;
}
p {
padding: 10px;
}
#wrapper {
margin: 0 auto;
width: 1000px;
}
#headerwrap {
width: 1000px;
float: left;
margin: 0 auto;
}
#header {
color: #fff;
background: #4d394b;
border-radius: 10px;
border: 1px solid #392537;
margin: 5px;
}
+#header a {
+ color: #fff;
+}
#header h1 {
font-size: 130%;
- padding: 10px;
+ padding: 10px;
}
+
#header nav{
display:block;
float:right;
margin:10px 0 0 0;
padding:20px 0;
color:#ffa500;
}
#header nav ul{
padding:0 20px;
}
#header nav li{
display:inline;
margin-right:25px;
text-transform:uppercase;
}
#header nav li.last{
margin-right:0;
}
#header nav li a{
text-decoration:none;
color:#ffa500;
}
#header nav li a:hover{
color:#fff;
}
#contentwrap {
width: 1000;
float: left;
margin: 0 auto;
}
#content {
background: #e5e4e0;
border-radius: 10px;
border: 1px solid #d1d0cc;
margin: 5px;
padding: 10px;
}
progress {
background: #e5e4e0;
border: none;
width: 200px;
}
progress.jaugeRouge::-moz-progress-bar { background: #FF785B; }
progress.jaugeOrange::-moz-progress-bar { background: #FFBB3E; }
progress.jaugeVerte::-moz-progress-bar { background: #76D176; }
progress.jaugeBleu::-moz-progress-bar { background: #8590F7; }
.waitFirst {
width: 100%;
text-align:center;
}
.box {
margin: 8px;
width: 300px;
background-color: #fff;
border-radius: 10px 10px 0 0;
float: left;
}
.box .plus {
display: none;
}
.box .plusboutton,
.box .moinsboutton {
text-align: center;
font-size: 80%;
cursor: pointer;
}
.box .moinsboutton {
display: none;
}
.box .title {
border-radius: 10px 10px 0 0;
background-color:#ffa500;
padding: 5px;
}
.box .boxvaleur {
padding: 5px;
border-bottom: 1px solid #BCB7B7;
color : #6B6B6B;
}
.box h3 {
font-size: 80%;
}
.box .ppv,
.box .ppvt {
height: 28px;
padding-left: 38px;
background-image: url("../images/PPV.png");
background-position: left top;
background-repeat: no-repeat;
}
.box .vbat {
height: 28px;
padding-left: 38px;
background-image: url("../images/VBAT.png");
background-position: left top;
background-repeat: no-repeat;
}
.box .cs,
.box .soc {
height: 28px;
padding-left: 38px;
background-image: url("../images/CS.png");
background-position: left top;
background-repeat: no-repeat;
}
.box .err,
.box .ar {
height: 28px;
padding-left: 38px;
background-image: url("../images/ERR.png");
background-position: left top;
background-repeat: no-repeat;
}
.souligner {
color: red;
font-weight: bold;
}
.box .temp {
height: 28px;
padding-left: 38px;
background-image: url("../images/TEMP.png");
background-position: left top;
background-repeat: no-repeat;
}
.box .conso {
height: 28px;
padding-left: 38px;
background-image: url("../images/CONSO.png");
background-position: left top;
background-repeat: no-repeat;
}
.box .boxvaleur {
margin: 3px;
}
#footerwrap {
width: 1000px;
float: left;
margin: 0 auto;
clear: both;
}
#footer {
color: #fff;
background: #4d394b;
border-radius: 10px;
border: 1px solid #392537;
margin: 5px;
}
#footer a {
color: #fff;
text-decoration:none;
}
.footer_right {
float: right;
}
@media screen and (max-width: 1000px) {
#headerwrap {
width: 100%;
}
#wrapper {
width: 100%;
}
#footerwrap {
width: 100%;
}
}
@media screen and (max-width: 600px) {
.box .plusplus {
display: none;
}
}
diff --git a/www/index.php b/www/index.php
index 0b467bb..d3ebd9b 100755
--- a/www/index.php
+++ b/www/index.php
@@ -1,305 +1,333 @@
Pv Monit
-
Pv Monit
+
Pv Monit v= VERSION ?>
Monitoring de l'installation solaire électrique
Patience...
';
echo '
Erreur
';
echo '
Heure du système :
';
echo '';
echo 'incorrect, on ne collecte rien.';
echo '';
echo '
';
echo '
';
}
?>
Rien à afficher, vérifier le fichier config.yaml. :