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). ![Schéma de câblage PI3B et ve.direct USB officiel](https://david.mercereau.info/wp-content/uploads/2019/10/PvMonitV1_USB.png)- 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 ![Schéma de câblage avec Pi0 et Arduino Mega (ve.direct Diy)](https://david.mercereau.info/wp-content/uploads/2019/10/PvMonitV1_Arduino.png) 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 : ![Schéma de câble Pi0-ArduinoMega](https://david.mercereau.info/wp-content/uploads/2019/10/PvMonit-1.0_bb.png) 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...) ![Screenshot PvMonit](http://david.mercereau.info/wp-content/uploads/2016/11/PvMonit_Full.png) #### 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 : ![Screenshot input API emoncms](http://david.mercereau.info/wp-content/uploads/2016/11/Sélection_011.png) 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... ![Screenshot source config emoncms](http://david.mercereau.info/wp-content/uploads/2016/11/emoncms_source_config.png) Voici, pour exemple, mon dashboard : http://emoncms.mercereau.info/dashboard/view?id=1 Une capture : ![Screenshot emoncms dashboard](http://david.mercereau.info/wp-content/uploads/2016/11/emoncms-mon-dashboard-pvmonit.png) #### 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 ![Ecran Adafruit LCD](https://david.mercereau.info/wp-content/uploads/2019/10/P1030685-e1571260899411.jpg) 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: -
  • EmonCMS (historique)
  • -
  • PvMonit projet
  • -
  • Soutenir l'auteur
  • + 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
    Patience...
    '; echo '
    Erreur
    '; echo '

    Heure du système :

    '; echo ''; echo 'incorrect, on ne collecte rien.'; echo ''; echo '
    '; echo '
    '; } ?>