diff --git a/abbb2x/.claude/REPRISE-ABB-atelier.md b/abbb2x/.claude/REPRISE-ABB-atelier.md new file mode 100644 index 0000000..ab29f3f --- /dev/null +++ b/abbb2x/.claude/REPRISE-ABB-atelier.md @@ -0,0 +1,98 @@ +# REPRISE — Test atelier : borne ABB Terra AC + compteur ABB B2x (B21/B23/B24) + +## Contexte +Je teste EN ATELIER une borne ABB Terra AC + un compteur ABB B2x (en stock) avant +une visite client. Objectif : faire reconnaître les deux par nymea, depuis MES +propres paquets Debian, et valider les mesures (surtout le scaling du compteur). + +On a rôdé tout le pipeline modbus aujourd'hui (plugin Eastron SDM construit et +fonctionnel). Les deux plugins ABB suivent EXACTEMENT le même chemin. + +## Infrastructure (rappel) +- VM build : etm-powersync-dev, conteneur LXC `build-1-15` (libnymea-dev 1.15.0) + - déjà installés dans le conteneur : libnymea-modbus-dev, qt6-serialport-dev, + qt6-serialbus-dev, nymea-dev-tools, qt6-base-dev(-tools) +- Edge de test : ssh etm@192.168.1.120 (etm-nymea-dev), nymead actif +- Dépôt APT : reprepro sur /mnt/builddisk/apt-repo ; publish-to-repo.sh ; clé GPG ETM + - canal de travail : powersync-testing ; edge branché dessus +- Repo modbus : git.etm-powersync.fr/ETM-Schurig/etm-powersync-plugins-modbus + - chemin local VM : ~/projects/etm-powersync/etm/etm-powersync-plugins-modbus + - contient déjà : eastron/ (fonctionne), modbus.pri recâblé sur paquets système + (PKGCONFIG += nymea-modbus + include(/usr/include/nymea-modbus/modbus-tool.pri)) + +## Règles de packaging (acquises aujourd'hui, à respecter) +- 1 seul paquet binaire par repo modbus actuel → PAS de fichier debian/*.install + (qmake6 installe la .so, debhelper la ramasse). Le .install ne sert qu'en multi-binaire. +- debian/control Build-Depends modbus : debhelper, pkg-config, libnymea-dev, + nymea-dev-tools:native, libnymea-modbus-dev, qt6-base-dev, qt6-base-dev-tools, + qt6-serialport-dev, qt6-serialbus-dev +- debian/rules : nettoyer autogenerated/ + moc_* + *plugininfo.h au clean +- changelog : format strict (ligne vide avant le " --"), version +etm1 (plugins maison) +- .pro racine : PLUGIN_DIRS sans backslash après la dernière entrée +- nom paquet plugin maison = powersync-plugin-XXX (pas de Provides/Replaces si code original) +- Le conteneur CLONE depuis Gitea → toujours git push AVANT de builder dans le conteneur + +## Le pipeline qui marche (référence eastron) +git push, puis dans le conteneur : + cd /root && rm -rf && git clone && cd + qmake6 && make -j$(nproc) # valider compil + génération modbus AVANT debian/ + dpkg-buildpackage -b -us -uc +Puis vérif (dpkg-deb -c → la .so présente), lxc file pull, reprepro includedeb +powersync-testing, publish-to-repo.sh, install sur .120, restart nymead. + +## TÂCHE 1 — Borne ABB Terra AC (code upstream, faible risque) +Le plugin existe DÉJÀ : nymea-plugins-modbus, branche experimental-silo, dossier abbterra/ +(deux ThingClass : terraAcTcp {address, port=502, slaveId=1} et terraAcRtu {rtuMaster, slaveId=1}). +À faire : +1. Copier abbterra/ depuis l'upstream experimental-silo dans MON repo modbus. +2. Ajouter "abbterra" à PLUGIN_DIRS (sans casser le backslash). +3. Builder via le pipeline ci-dessus → powersync-plugin-abbterra_1.15.0+etm1. +4. Import testing + install .120. +À CONFIRMER en atelier : la borne en stock est-elle en Modbus TCP ou RTU ? +(détermine quelle ThingClass utiliser à la config nymea:app) + +## TÂCHE 2 — Compteur ABB B2x (plugin NEUF, à valider sur matériel) +J'ai déjà un abbb2x-registers.json prêt (registres extraits/vérifiés des manuels +B21 + B23/B24 — même mapping pour les 3 modèles ; puissances en int32 SIGNÉ pour +gérer l'injection PV ; énergie import/export en uint64 size 4 ; scaling via "unit"). +Reste à créer le plugin autour : +1. Créer le dossier b2x/ (ou abbb2x/) dans MON repo modbus avec : + - abbb2x-registers.json (je l'ai) + - eastron.pro → adapter en abbb2x.pro (MODBUS_CONNECTIONS += abbb2x-registers.json ; + include(../modbus.pri)) + - integrationpluginabbb2x.cpp/.h/.json → ADAPTER depuis integrationplugineastron.* + (même type d'appareil = compteur ; mapper les registres générés vers l'interface + nymea smartmeter : currentPower, voltage/current/power par phase, énergie conso/prod). + Vendor = "ABB". Nouveau pluginId/thingClassId/vendorId (générer des UUID, NE PAS + réutiliser ceux d'eastron). +2. Ajouter "abbb2x" à PLUGIN_DIRS. +3. Builder → powersync-plugin-abbb2x_1.15.0+etm1, import, install .120. + +POINTS À VALIDER FACE AU COMPTEUR (typiques d'un plugin neuf) : +- SCALING PUISSANCE : manuel dit "Signed, 0.01 W". Si la puissance lue est ×100 trop + grande, le scaling réel est 1W → corriger le "unit" dans le registers.json. +- Comment le pipeline applique le "unit" (générateur vs code .cpp) : vérifier dans + integrationpluginabbterra.cpp comment ABB applique le scaling (grep unit/toDouble/setVoltage). +- checkReachableRegister = voltagePhaseA : confirmer qu'il répond à l'init. +- Sur un compteur monophasé (B21), L2/L3 doivent rester à 0/invalide — normal. + +## Config Modbus RTU (si transport série) +- Vérifier l'adaptateur série sur .120 : ls -l /dev/ttyUSB* ; dmesg | grep -iE "ttyUSB|ftdi|ch341|cp210" +- nymead doit avoir accès au port : user nymea dans le groupe dialout (sudo usermod -aG dialout nymea + restart) +- Paramètres ligne ABB B2x par défaut : à confirmer sur l'écran du compteur (souvent 9600 8N1) +- Adresses esclaves : si borne RTU + compteur RTU sur le MÊME bus → slaveId DISTINCTS +- Dans nymea:app : si RTU, créer d'abord le "Modbus RTU master" (l'adaptateur), puis les things + +## À ne pas oublier (notes de fond, PAS pour aujourd'hui) +- Modélisation interfaces compteur : prévoir au ThingClass un param "role" + (producteur/consommateur/raccordement) + un param "phase" (L1/L2/L3 pour mono) — + donnée d'installation, inerte aujourd'hui, prérequis du DÉLESTAGE futur. +- Délestage : stratégie 2 niveaux (N1 = phases du compteur principal ; N2 = sous-compteurs + par circuit). nymea-energy-plugin-nymea (chargebyte, GPL) contient déjà de la logique de + délestage → l'INVESTIGUER avant de coder. Le délestage ira dans powersync-energy-plugin-etm + (Community, rule-based, GPL) ; l'optimizer reste propriétaire (œuvre séparée via socket). + +## Mon environnement +Je travaille en français. Je préfère : affichage complet des fichiers (cat -n) plutôt que +diffs, logs complets avant tout commit, pas de fallback silencieux. Va étape par étape, +attends ma sortie de commande avant de passer à la suivante. diff --git a/abbterra/.claude/REPRISE-ABB-terraac.md b/abbterra/.claude/REPRISE-ABB-terraac.md new file mode 100644 index 0000000..0a4c7cd --- /dev/null +++ b/abbterra/.claude/REPRISE-ABB-terraac.md @@ -0,0 +1,153 @@ +# REPRISE — TÂCHE 1 : intégrer & builder ABB Terra AC (vendoring upstream) + +## Contexte +Je prépare une visite client (borne ABB Terra AC + compteur ABB B23). Test EN ATELIER +d'abord. Le plugin **abbterra** se trouve DÉJÀ dans MON repo +`etm-powersync-plugins-modbus` (dossier `abbterra/`) — il a été copié depuis l'upstream +nymea-plugins-modbus, branche `experimental-silo`. Le compteur B2x (TÂCHE 2) est déjà +intégré et prêt (abbb2x ajouté à PLUGIN_DIRS, debian/control + changelog faits). + +But de cette tâche : packager et déployer la borne, proprement, en décidant comment on +gère le fait que ce code est upstream-mais-pas-encore-release. + +## Décision de fond (déjà tranchée — à appliquer, pas à rediscuter) +abbterra N'EST PAS un fork divergent : je ne modifie pas le code, je le **builde en avance** +parce que nymea ne l'a pas encore publié dans son dépôt apt (il n'est que dans la branche +experimental-silo). C'est du **vendoring temporaire**. Tout nymea est GPLv3 → redistribution +et build anticipé explicitement permis, aucune contrainte juridique. + +Approche retenue (cohérente avec l'archi existante du mirror) : +1. **Nom du paquet = `nymea-plugin-abbterra`** (nom UPSTREAM, PAS de préfixe powersync-). +2. **PAS de Provides/Replaces/Conflicts** (rien ne le concurrence : le mirror l'exclura). +3. **Ajouter `abbterra` à `FORKED_PLUGINS`** dans `/mnt/builddisk/sync-nymea-mirror.sh`. + → empêche le mirror de réimporter une version upstream concurrente sous le même nom. + `FORKED_PLUGINS` couvre désormais DEUX cas : forks divergents (keba) ET builds + anticipés de code upstream non encore release (abbterra). Mettre à jour son commentaire. +4. **Tracer le vendoring** : créer `abbterra/VENDORED.md` (voir contenu plus bas). + +Pourquoi ce choix : transition douce. Le jour où nymea release abbterra dans master/stable, +il suffira de (a) supprimer le dossier abbterra/ de mon repo, (b) retirer `abbterra` de +FORKED_PLUGINS, (c) relancer le sync — le mirror tirera alors la version OFFICIELLE sous +le MÊME nom, donc l'edge bascule dessus sans réinstall ni reconfig des things. Un seul nom +de paquet existe à tout instant, jamais de doublon. + +## Infrastructure (rappel) +- VM build : etm-powersync-dev ; conteneur LXC `build-1-15` (libnymea-dev 1.15.0, + libnymea-modbus-dev, qt6-serialport-dev, qt6-serialbus-dev, nymea-dev-tools, python3). +- Edge test : ssh etm@192.168.1.120, nymead actif, canal powersync-testing. +- Dépôt APT : reprepro /mnt/builddisk/apt-repo ; publish-to-repo.sh ; clé GPG ETM. +- Mirror : /mnt/builddisk/sync-nymea-mirror.sh (sélection auto depuis index upstream + moins FORKED_PLUGINS ; keba déjà dedans). +- Repo modbus : git.etm-powersync.fr/ETM-Schurig/etm-powersync-plugins-modbus + local : ~/projects/etm-powersync/etm/etm-powersync-plugins-modbus + - contient déjà : eastron/ (OK, en prod), abbb2x/ (prêt), abbterra/ (à packager). + - modbus.pri recâblé sur paquets système (PKGCONFIG nymea-modbus + modbus-tool.pri). + - Le conteneur CLONE depuis Gitea → TOUJOURS git push avant de builder. + +## Règles de packaging (acquises, à respecter) +- .pro racine : PLUGIN_DIRS une entrée par ligne, PAS de backslash après la dernière ; + pas de SUBDIRS local ni de .depends vers libnymea-modbus (lib système). +- Multi-binaire : le repo aura maintenant 3 paquets (eastron + abbb2x + abbterra) dans + le MÊME debian/ → IL FAUT un `debian/.install` par paquet (nom EXACT du Package:), + sinon dh_install ne route pas les .so → paquet vide. Vérifier que les .install existent + pour les 3 : powersync-plugin-eastron.install, powersync-plugin-abbb2x.install, + nymea-plugin-abbterra.install. Chacun contient la ligne : + usr/lib/*/nymea/plugins/libnymea_integrationplugin.so +- debian/control Build-Depends modbus : debhelper, pkg-config, libnymea-dev, + nymea-dev-tools:native, libnymea-modbus-dev, qt6-base-dev, qt6-base-dev-tools, + qt6-serialport-dev, qt6-serialbus-dev. +- changelog : format strict (ligne vide avant le " --"). Bumper la source en +etm3 + (etm2 = ajout abbb2x déjà fait). +- rules : nettoyer autogenerated/ + moc_* + *plugininfo.h au dh_auto_clean. + +## ÉTAPES + +### 1. Vérifier l'état d'abbterra dans le repo + ls -la ~/projects/.../etm-powersync-plugins-modbus/abbterra/ + grep -n 'PLUGIN_DIRS\|abbterra\|abbb2x\|eastron' etm-powersync-plugins-modbus.pro + - abbterra présent ? abbterra dans PLUGIN_DIRS ? (l'ajouter si absent, sans casser le backslash) + +### 2. debian/ — ajouter le paquet nymea-plugin-abbterra + - debian/control : nouveau stanza `Package: nymea-plugin-abbterra` + Architecture: any + Section: libs + Depends: ${shlibs:Depends}, ${misc:Depends} + (PAS de Provides/Replaces/Conflicts) + Description: ABB Terra AC charging station (Modbus TCP/RTU) — vendored from + nymea-plugins-modbus experimental-silo, pending upstream release. + - debian/nymea-plugin-abbterra.install : + usr/lib/*/nymea/plugins/libnymea_integrationpluginabbterra.so + - debian/changelog : nouvelle entrée en tête, version 1.15.0+etm3 (ligne vide avant " --"). + - Vérifier que les .install des 3 paquets existent (cf. règles multi-binaire). + +### 3. Tracer le vendoring — créer abbterra/VENDORED.md + Contenu : + ---------------------------------------------------------------- + # Vendoring — abbterra + Copié depuis : nymea/nymea-plugins-modbus @ branche experimental-silo + Commit source : a652793 ("Add new plugin for ABB Terra AC Charger") + Date copie : 2026-06-01 + Raison : plugin présent upstream mais PAS encore publié dans le dépôt apt nymea. + Build anticipé ETM en attendant la release master/stable. + Nom paquet : nymea-plugin-abbterra (nom upstream conservé) + Exclu du mirror via FORKED_PLUGINS dans sync-nymea-mirror.sh. + SORTIE (quand nymea release abbterra dans master/stable) : + 1) supprimer le dossier abbterra/ de ce repo + 2) retirer "abbterra" de FORKED_PLUGINS + 3) relancer sync-nymea-mirror.sh → le mirror tire la version officielle (même nom) + ---------------------------------------------------------------- + +### 4. Mirror — exclure abbterra + Dans /mnt/builddisk/sync-nymea-mirror.sh, dans FORKED_PLUGINS : + FORKED_PLUGINS=( + "keba" + "abbterra" + ) + Mettre à jour le commentaire au-dessus pour préciser les 2 cas (fork divergent + vendoring). + +### 5. Build (pipeline validé) + git add -A && git commit -m "..." && git push + sudo lxc exec build-1-15 -- bash -c ' + set -e + cd /root && rm -rf etm-powersync-plugins-modbus + git clone https://git.etm-powersync.fr/ETM-Schurig/etm-powersync-plugins-modbus.git + cd etm-powersync-plugins-modbus + echo "=== plugins ===" && grep -A6 PLUGIN_DIRS etm-powersync-plugins-modbus.pro + echo "=== installs ===" && ls debian/*.install + echo "=== packages ===" && grep -c "^Package:" debian/control + chmod +x debian/rules + # build local d abord pour valider compil + generation, PUIS le .deb : + qmake6 && make -j$(nproc) 2>&1 | tail -30 + ' + - Vérifier dans la sortie : abbterra ET abbb2x ET eastron compilent, .so produites. + - Si abbterra casse sur des getters/connexion : c est du code upstream testé, donc + plutot un souci d intégration (PLUGIN_DIRS, modbus.pri) qu un bug — lire l erreur. + - Puis : dpkg-buildpackage -b -us -uc 2>&1 | tail -30 + +### 6. Vérifier les .deb (anti-paquet-vide) + Pour chacun des 3 : dpkg-deb -c | grep '\.so' → une .so au bon chemin. + Pour abbterra : dpkg-deb -f nymea-plugin-abbterra_*.deb Package Depends + (Depends doit inclure libqt6serialbus/serialport via shlibs). + +### 7. Import + déploiement + lxc file pull des .deb vers /mnt/builddisk + reprepro -b /mnt/builddisk/apt-repo includedeb powersync-testing (les 3 + dbgsym) + /mnt/builddisk/publish-to-repo.sh + reprepro -b /mnt/builddisk/apt-repo list powersync-testing | grep -iE 'abbterra|abbb2x' + ssh etm@192.168.1.120 'sudo apt update && sudo apt install -y nymea-plugin-abbterra powersync-plugin-abbb2x && sudo systemctl restart nymead' + +## Config en atelier (après install) +- Borne ABB Terra : ThingClass terraAcTcp (address, port 502, slaveId 1) OU terraAcRtu + (rtuMaster, slaveId 1). CONFIRMER sur le matériel : TCP (réseau) ou RTU (RS-485) ? +- Compteur ABB B2x : RTU (rtuMaster + slaveAddress). Si borne RTU + compteur RTU sur le + MÊME bus → slaveId DISTINCTS. +- RTU : créer d'abord le "Modbus RTU master" (adaptateur /dev/ttyUSB*) dans nymea:app, + vérifier droits (user nymead dans groupe dialout), puis ajouter les things. +- B2x = code NEUF : valider scaling (puissance signée +import/-export ; si ×100 trop grand + passer /100 à /1 dans le .cpp ; vérifier noms getters générés dans + abbb2x/autogenerated/abbb2xmodbusrtuconnection.h). + +## Préférences de travail +Français. Affichage complet des fichiers (cat -n) plutôt que diffs ; logs complets avant +commit ; pas de fallback silencieux. Étape par étape : attendre ma sortie de commande avant +de continuer. Toujours vérifier le contenu d'un .deb (dpkg-deb -c) avant import. diff --git a/abbterra/README.md b/abbterra/README.md new file mode 100644 index 0000000..5b236e1 --- /dev/null +++ b/abbterra/README.md @@ -0,0 +1,19 @@ +# ABB Terra AC + +This plugin integrates ABB Terra AC chargers via Modbus TCP and Modbus RTU. + +Implemented features: + +- network discovery for Modbus TCP chargers +- Modbus RTU discovery using nymea's managed RTU hardware resource +- connection state +- plugged-in and charging state detection +- charging enable/disable via current-limit control +- writable maximum charging current +- active power and session energy +- firmware version, serial number, and error code +- communication timeout setting + +The register model is based on: + +- `ABB_Terra_AC_Charger_ModbusCommunication_v1.7.pdf` diff --git a/abbterra/VENDORED.md b/abbterra/VENDORED.md new file mode 100644 index 0000000..145d085 --- /dev/null +++ b/abbterra/VENDORED.md @@ -0,0 +1,12 @@ +# Vendoring — abbterra +Copié depuis : nymea/nymea-plugins-modbus @ branche experimental-silo +Commit source : a652793 ("Add new plugin for ABB Terra AC Charger") +Date copie : 2026-06-01 +Raison : plugin présent upstream mais PAS encore publié dans le dépôt apt nymea. + Build anticipé ETM en attendant la release master/stable. +Nom paquet : nymea-plugin-abbterra (nom upstream conservé) +Exclu du mirror via FORKED_PLUGINS dans sync-nymea-mirror.sh. +SORTIE (quand nymea release abbterra dans master/stable) : + 1) supprimer le dossier abbterra/ de ce repo + 2) retirer "abbterra" de FORKED_PLUGINS + 3) relancer sync-nymea-mirror.sh → le mirror tire la version officielle (même nom) diff --git a/abbterra/abbterra-registers.json b/abbterra/abbterra-registers.json new file mode 100644 index 0000000..0bf1e80 --- /dev/null +++ b/abbterra/abbterra-registers.json @@ -0,0 +1,243 @@ +{ + "className": "AbbTerra", + "protocol": "BOTH", + "endianness": "BigEndian", + "stringEndianness": "BigEndian", + "errorLimitUntilNotReachable": 3, + "checkReachableRegister": "serialNumber", + "queuedRequests": true, + "queuedRequestsDelay": 50, + "blocks": [ + { + "id": "deviceInfo", + "readSchedule": "init", + "registers": [ + { + "id": "serialNumber", + "address": 16384, + "size": 4, + "type": "uint64", + "registerType": "holdingRegister", + "description": "Product serial number", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "firmwareVersionRaw", + "address": 16388, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Firmware version", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "userSettableMaxCurrent", + "address": 16390, + "size": 2, + "type": "uint32", + "unit": "mA", + "registerType": "holdingRegister", + "description": "Maximum user settable charging current", + "defaultValue": "32000", + "access": "RO" + } + ] + }, + { + "id": "status", + "readSchedule": "update", + "registers": [ + { + "id": "errorCode", + "address": 16392, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Last error code", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "socketLockState", + "address": 16394, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Socket and cable lock state", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "chargingStateRaw", + "address": 16396, + "size": 2, + "type": "uint32", + "registerType": "holdingRegister", + "description": "Charging state", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "chargingCurrentLimit", + "address": 16398, + "size": 2, + "type": "uint32", + "unit": "mA", + "registerType": "holdingRegister", + "description": "Current charging current limit", + "defaultValue": "6000", + "access": "RO" + }, + { + "id": "currentL1", + "address": 16400, + "size": 2, + "type": "uint32", + "unit": "mA", + "registerType": "holdingRegister", + "description": "Current L1", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "currentL2", + "address": 16402, + "size": 2, + "type": "uint32", + "unit": "mA", + "registerType": "holdingRegister", + "description": "Current L2", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "currentL3", + "address": 16404, + "size": 2, + "type": "uint32", + "unit": "mA", + "registerType": "holdingRegister", + "description": "Current L3", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "voltageL1", + "address": 16406, + "size": 2, + "type": "uint32", + "unit": "0.1V", + "registerType": "holdingRegister", + "description": "Voltage L1", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "voltageL2", + "address": 16408, + "size": 2, + "type": "uint32", + "unit": "0.1V", + "registerType": "holdingRegister", + "description": "Voltage L2", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "voltageL3", + "address": 16410, + "size": 2, + "type": "uint32", + "unit": "0.1V", + "registerType": "holdingRegister", + "description": "Voltage L3", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "activePower", + "address": 16412, + "size": 2, + "type": "uint32", + "unit": "W", + "registerType": "holdingRegister", + "description": "Measured active power", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "sessionEnergy", + "address": 16414, + "size": 2, + "type": "uint32", + "unit": "Wh", + "registerType": "holdingRegister", + "description": "Delivered energy of the current session", + "defaultValue": "0", + "access": "RO" + }, + { + "id": "communicationTimeoutReadback", + "address": 16416, + "size": 1, + "type": "uint16", + "unit": "s", + "registerType": "holdingRegister", + "description": "Communication timeout", + "defaultValue": "60", + "access": "RO" + } + ] + } + ], + "registers": [ + { + "id": "chargingCurrentLimitCommand", + "address": 16640, + "size": 2, + "type": "uint32", + "unit": "mA", + "readSchedule": "", + "registerType": "holdingRegister", + "description": "Set charging current limit", + "defaultValue": "6000", + "access": "WO" + }, + { + "id": "socketLockCommand", + "address": 16642, + "size": 1, + "type": "uint16", + "readSchedule": "", + "registerType": "holdingRegister", + "description": "Socket lock control", + "defaultValue": "0", + "access": "WO" + }, + { + "id": "startStopChargingSession", + "address": 16645, + "size": 1, + "type": "uint16", + "readSchedule": "", + "registerType": "holdingRegister", + "description": "Start or stop charging session", + "defaultValue": "0", + "access": "WO" + }, + { + "id": "communicationTimeoutCommand", + "address": 16646, + "size": 1, + "type": "uint16", + "unit": "s", + "readSchedule": "", + "registerType": "holdingRegister", + "description": "Set communication timeout", + "defaultValue": "60", + "access": "WO" + } + ] +} diff --git a/abbterra/abbterra.pro b/abbterra/abbterra.pro new file mode 100644 index 0000000..c515ac3 --- /dev/null +++ b/abbterra/abbterra.pro @@ -0,0 +1,20 @@ +include(../plugins.pri) + +MODBUS_CONNECTIONS += abbterra-registers.json +#MODBUS_TOOLS_CONFIG += VERBOSE +include(../modbus.pri) + +TARGET = $$qtLibraryTarget(nymea_integrationpluginabbterra) + +OTHER_FILES += abbterra-registers.json + +SOURCES += \ + abbterrartudiscovery.cpp \ + abbterratcpdiscovery.cpp \ + integrationpluginabbterra.cpp + +HEADERS += \ + abbterrartudiscovery.h \ + abbterratcpdiscovery.h \ + abbterrautils.h \ + integrationpluginabbterra.h diff --git a/abbterra/abbterrartudiscovery.cpp b/abbterra/abbterrartudiscovery.cpp new file mode 100644 index 0000000..33078ea --- /dev/null +++ b/abbterra/abbterrartudiscovery.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "abbterrartudiscovery.h" +#include "abbterrautils.h" +#include "extern-plugininfo.h" + +AbbTerraRtuDiscovery::AbbTerraRtuDiscovery(ModbusRtuHardwareResource *modbusRtuResource, QObject *parent) + : QObject(parent), + m_modbusRtuResource(modbusRtuResource) +{ +} + +void AbbTerraRtuDiscovery::startDiscovery() +{ + qCInfo(dcAbbTerra()) << "Discovery: Searching for ABB Terra AC chargers on Modbus RTU..."; + + m_candidateMasters.clear(); + m_results.clear(); + m_masterIndex = 0; + m_slaveId = 1; + + foreach (ModbusRtuMaster *master, m_modbusRtuResource->modbusRtuMasters()) { + if (master->connected()) { + m_candidateMasters.append(master); + } + } + + if (m_candidateMasters.isEmpty()) { + qCWarning(dcAbbTerra()) << "No connected Modbus RTU master available for ABB Terra AC discovery."; + emit discoveryFinished(false); + return; + } + + scanNext(); +} + +QList AbbTerraRtuDiscovery::results() const +{ + return m_results; +} + +void AbbTerraRtuDiscovery::scanNext() +{ + if (m_masterIndex >= m_candidateMasters.count()) { + emit discoveryFinished(true); + return; + } + + if (m_slaveId > 247) { + m_masterIndex++; + m_slaveId = 1; + scanNext(); + return; + } + + ModbusRtuMaster *master = m_candidateMasters.at(m_masterIndex); + const quint16 currentSlaveId = m_slaveId++; + + ModbusRtuReply *reply = master->readHoldingRegister(currentSlaveId, 0x4000, 8); + connect(reply, &ModbusRtuReply::finished, this, [this, master, currentSlaveId, reply]() { + if (reply->error() == ModbusRtuReply::NoError) { + const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromRegisters(reply->result()); + if (deviceInfo.valid) { + Result result; + result.modbusRtuMasterId = master->modbusUuid(); + result.slaveId = currentSlaveId; + result.serialNumber = deviceInfo.serialNumber; + result.productName = deviceInfo.productName; + result.firmwareVersion = deviceInfo.firmwareVersion; + m_results.append(result); + } + } + + scanNext(); + }); +} diff --git a/abbterra/abbterrartudiscovery.h b/abbterra/abbterrartudiscovery.h new file mode 100644 index 0000000..ddeac2f --- /dev/null +++ b/abbterra/abbterrartudiscovery.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef ABBTERRARTUDISCOVERY_H +#define ABBTERRARTUDISCOVERY_H + +#include + +#include + +class AbbTerraRtuDiscovery : public QObject +{ + Q_OBJECT +public: + struct Result { + QUuid modbusRtuMasterId; + quint16 slaveId; + QString serialNumber; + QString productName; + QString firmwareVersion; + }; + + explicit AbbTerraRtuDiscovery(ModbusRtuHardwareResource *modbusRtuResource, QObject *parent = nullptr); + + void startDiscovery(); + QList results() const; + +signals: + void discoveryFinished(bool modbusRtuMasterAvailable); + +private: + void scanNext(); + +private: + ModbusRtuHardwareResource *m_modbusRtuResource = nullptr; + QList m_candidateMasters; + QList m_results; + int m_masterIndex = 0; + quint16 m_slaveId = 1; +}; + +#endif // ABBTERRARTUDISCOVERY_H diff --git a/abbterra/abbterratcpdiscovery.cpp b/abbterra/abbterratcpdiscovery.cpp new file mode 100644 index 0000000..e0077f1 --- /dev/null +++ b/abbterra/abbterratcpdiscovery.cpp @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "abbterratcpdiscovery.h" +#include "abbterrautils.h" +#include "extern-plugininfo.h" + +#include +#include +#include + +AbbTerraTcpDiscovery::AbbTerraTcpDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) + : QObject(parent), + m_networkDeviceDiscovery(networkDeviceDiscovery) +{ +} + +void AbbTerraTcpDiscovery::startDiscovery() +{ + qCInfo(dcAbbTerra()) << "Discovery: Starting to search for ABB Terra AC chargers on the network..."; + + m_startDateTime = QDateTime::currentDateTime(); + m_networkDeviceInfos.clear(); + m_temporaryResults.clear(); + m_results.clear(); + + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::hostAddressDiscovered, this, &AbbTerraTcpDiscovery::checkNetworkDevice); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, discoveryReply, &NetworkDeviceDiscoveryReply::deleteLater); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [this, discoveryReply]() { + m_networkDeviceInfos = discoveryReply->networkDeviceInfos(); + QTimer::singleShot(3000, this, &AbbTerraTcpDiscovery::finishDiscovery); + }); +} + +QList AbbTerraTcpDiscovery::results() const +{ + return m_results; +} + +void AbbTerraTcpDiscovery::checkNetworkDevice(const QHostAddress &address) +{ + AbbTerraModbusTcpConnection *connection = new AbbTerraModbusTcpConnection(address, 502, 1, this); + m_connections.append(connection); + + connect(connection, &AbbTerraModbusTcpConnection::reachableChanged, this, [this, connection](bool reachable) { + if (!reachable) { + cleanupConnection(connection); + return; + } + + connection->initialize(); + }); + + connect(connection, &AbbTerraModbusTcpConnection::initializationFinished, this, [this, connection, address](bool success) { + if (!success) { + cleanupConnection(connection); + return; + } + + const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromValues(connection->serialNumber(), + connection->firmwareVersionRaw(), + connection->userSettableMaxCurrent()); + if (deviceInfo.valid) { + Result result; + result.serialNumber = deviceInfo.serialNumber; + result.productName = deviceInfo.productName; + result.firmwareVersion = deviceInfo.firmwareVersion; + result.networkDeviceInfo = m_networkDeviceInfos.get(address); + if (result.networkDeviceInfo.address().isNull()) { + NetworkDeviceInfo info; + info.setAddress(address); + result.networkDeviceInfo = info; + } + m_temporaryResults.append(result); + } + + cleanupConnection(connection); + }); + + connect(connection->modbusTcpMaster(), &ModbusTcpMaster::connectionErrorOccurred, this, [this, connection](QModbusDevice::Error error) { + if (error != QModbusDevice::NoError) { + cleanupConnection(connection); + } + }); + + connect(connection, &AbbTerraModbusTcpConnection::checkReachabilityFailed, this, [this, connection]() { + cleanupConnection(connection); + }); + + connection->connectDevice(); +} + +void AbbTerraTcpDiscovery::cleanupConnection(AbbTerraModbusTcpConnection *connection) +{ + m_connections.removeAll(connection); + connection->disconnectDevice(); + connection->deleteLater(); +} + +void AbbTerraTcpDiscovery::finishDiscovery() +{ + foreach (const Result &result, m_temporaryResults) { + bool known = false; + foreach (const Result &existing, m_results) { + if (existing.serialNumber == result.serialNumber || existing.networkDeviceInfo.address() == result.networkDeviceInfo.address()) { + known = true; + break; + } + } + + if (!known) { + qCDebug(dcAbbTerra()) << "Discovery: Found" << result.productName << result.networkDeviceInfo; + m_results.append(result); + } + } + + const QList leftoverConnections = m_connections; + foreach (AbbTerraModbusTcpConnection *connection, leftoverConnections) { + cleanupConnection(connection); + } + + const qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch(); + qCInfo(dcAbbTerra()) << "Discovery: Finished ABB Terra AC network discovery in" + << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz") + << "with" << m_results.count() << "result(s)."; + emit discoveryFinished(); +} diff --git a/abbterra/abbterratcpdiscovery.h b/abbterra/abbterratcpdiscovery.h new file mode 100644 index 0000000..48dcda3 --- /dev/null +++ b/abbterra/abbterratcpdiscovery.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef ABBTERRATCPDISCOVERY_H +#define ABBTERRATCPDISCOVERY_H + +#include +#include + +#include + +#include "abbterramodbustcpconnection.h" + +class AbbTerraTcpDiscovery : public QObject +{ + Q_OBJECT +public: + struct Result { + NetworkDeviceInfo networkDeviceInfo; + QString serialNumber; + QString productName; + QString firmwareVersion; + }; + + explicit AbbTerraTcpDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + + void startDiscovery(); + QList results() const; + +signals: + void discoveryFinished(); + +private: + void checkNetworkDevice(const QHostAddress &address); + void cleanupConnection(AbbTerraModbusTcpConnection *connection); + void finishDiscovery(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + QDateTime m_startDateTime; + NetworkDeviceInfos m_networkDeviceInfos; + QList m_connections; + QList m_results; + QList m_temporaryResults; +}; + +#endif // ABBTERRATCPDISCOVERY_H diff --git a/abbterra/abbterrautils.h b/abbterra/abbterrautils.h new file mode 100644 index 0000000..07580ad --- /dev/null +++ b/abbterra/abbterrautils.h @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef ABBTERRAUTILS_H +#define ABBTERRAUTILS_H + +#include +#include + +#include + +namespace AbbTerraUtils { + +struct DeviceInfo { + bool valid = false; + QString serialNumber; + QString productName; + QString firmwareVersion; + double maxChargingCurrent = 32.0; +}; + +inline QString connectorTypeName(quint8 connectorType) +{ + switch (connectorType) { + case 'G': + return QStringLiteral("Cable"); + case 'P': + return QStringLiteral("Outlet"); + case 'S': + return QStringLiteral("Socket"); + case 'T': + return QStringLiteral("Socket"); + default: + return QString(); + } +} + +inline int ratedPowerFromCode(quint8 ratedPowerCode) +{ + switch (ratedPowerCode) { + case 7: + return 7; + case 11: + return 11; + case 22: + return 22; + default: + return -1; + } +} + +inline DeviceInfo deviceInfoFromValues(quint64 serialNumberRaw, quint32 firmwareVersionRaw, quint32 maxCurrentRaw) +{ + DeviceInfo deviceInfo; + + const quint8 connectorType = static_cast((serialNumberRaw >> 56) & 0xff); + const quint8 ratedPowerCode = static_cast((serialNumberRaw >> 48) & 0xff); + const quint8 plantId = static_cast((serialNumberRaw >> 40) & 0xff); + const quint8 productionWeek = static_cast((serialNumberRaw >> 24) & 0xff); + const quint8 productionYear = static_cast((serialNumberRaw >> 16) & 0xff); + const quint8 uniqueHigh = static_cast((serialNumberRaw >> 8) & 0xff); + const quint8 uniqueLow = static_cast(serialNumberRaw & 0xff); + const int ratedPower = ratedPowerFromCode(ratedPowerCode); + const QString connectorName = connectorTypeName(connectorType); + + if (ratedPower <= 0 || connectorName.isEmpty() || productionWeek == 0 || productionWeek > 53) { + return deviceInfo; + } + + deviceInfo.valid = true; + deviceInfo.serialNumber = QStringLiteral("TACW%1-%2-%3%4-%5%6%7") + .arg(ratedPower) + .arg(plantId) + .arg(productionWeek, 2, 10, QLatin1Char('0')) + .arg(productionYear, 2, 10, QLatin1Char('0')) + .arg(QChar(static_cast(connectorType))) + .arg(uniqueHigh, 2, 10, QLatin1Char('0')) + .arg(uniqueLow, 2, 10, QLatin1Char('0')); + deviceInfo.productName = QStringLiteral("ABB Terra AC %1 kW %2").arg(ratedPower).arg(connectorName); + deviceInfo.firmwareVersion = QStringLiteral("%1.%2.%3") + .arg((firmwareVersionRaw >> 24) & 0xff) + .arg((firmwareVersionRaw >> 16) & 0xff) + .arg((firmwareVersionRaw >> 8) & 0xff); + if (maxCurrentRaw >= 6000 && maxCurrentRaw <= 32000) { + deviceInfo.maxChargingCurrent = maxCurrentRaw / 1000.0; + } + + return deviceInfo; +} + +inline DeviceInfo deviceInfoFromRegisters(const QVector ®isters) +{ + if (registers.count() < 8) { + return DeviceInfo(); + } + + return deviceInfoFromValues( + ModbusDataUtils::convertToUInt64(registers.mid(0, 4), ModbusDataUtils::ByteOrderBigEndian), + ModbusDataUtils::convertToUInt32(registers.mid(4, 2), ModbusDataUtils::ByteOrderBigEndian), + ModbusDataUtils::convertToUInt32(registers.mid(6, 2), ModbusDataUtils::ByteOrderBigEndian) + ); +} + +inline quint8 chargingStateCode(quint32 chargingStateRaw) +{ + return static_cast((chargingStateRaw >> 8) & 0x7f); +} + +inline bool chargingLimitedByCar(quint32 chargingStateRaw) +{ + return ((chargingStateRaw >> 15) & 0x1) == 0x1; +} + +inline bool isVehiclePluggedIn(quint32 chargingStateRaw, quint32 socketLockState) +{ + const quint8 chargingState = chargingStateCode(chargingStateRaw); + if (chargingState >= 3 && chargingState <= 6) { + return true; + } + + return socketLockState == 0x0101 || socketLockState == 0x0111; +} + +inline bool isCharging(quint32 chargingStateRaw, quint32 activePower) +{ + return chargingStateCode(chargingStateRaw) == 6 || activePower > 100; +} + +inline uint phaseCount(quint32 voltageL1, quint32 voltageL2, quint32 voltageL3) +{ + uint phases = 0; + if (voltageL1 > 1000) { + phases++; + } + if (voltageL2 > 1000) { + phases++; + } + if (voltageL3 > 1000) { + phases++; + } + return qMax(phases, 1u); +} + +} // namespace AbbTerraUtils + +#endif // ABBTERRAUTILS_H diff --git a/abbterra/docs/ABB_Terra_AC_Charger_ModbusCommunication_v1.7.pdf b/abbterra/docs/ABB_Terra_AC_Charger_ModbusCommunication_v1.7.pdf new file mode 100644 index 0000000..4dd264a Binary files /dev/null and b/abbterra/docs/ABB_Terra_AC_Charger_ModbusCommunication_v1.7.pdf differ diff --git a/abbterra/integrationpluginabbterra.cpp b/abbterra/integrationpluginabbterra.cpp new file mode 100644 index 0000000..762181a --- /dev/null +++ b/abbterra/integrationpluginabbterra.cpp @@ -0,0 +1,483 @@ +#include "integrationpluginabbterra.h" +#include "abbterrartudiscovery.h" +#include "abbterratcpdiscovery.h" +#include "abbterrautils.h" + +#include "integrations/thing.h" +#include "integrations/thingactioninfo.h" +#include "integrations/thingdescriptor.h" +#include "integrations/thingdiscoveryinfo.h" +#include "integrations/thingsetupinfo.h" +#include "plugininfo.h" + +#include +#include +#include + +#include +#include +#include + +IntegrationPluginAbbterra::IntegrationPluginAbbterra() +{ +} + +void IntegrationPluginAbbterra::discoverThings(ThingDiscoveryInfo *info) +{ + if (info->thingClassId() == terraAcTcpThingClassId) { + AbbTerraTcpDiscovery *discovery = new AbbTerraTcpDiscovery(hardwareManager()->networkDeviceDiscovery(), info); + connect(discovery, &AbbTerraTcpDiscovery::discoveryFinished, info, [this, info, discovery]() { + foreach (const AbbTerraTcpDiscovery::Result &result, discovery->results()) { + ThingDescriptor descriptor(terraAcTcpThingClassId, result.productName, result.serialNumber); + ParamList params; + params.append(Param(terraAcTcpThingMacAddressParamTypeId, result.networkDeviceInfo.thingParamValueMacAddress())); + params.append(Param(terraAcTcpThingHostNameParamTypeId, result.networkDeviceInfo.thingParamValueHostName())); + params.append(Param(terraAcTcpThingAddressParamTypeId, result.networkDeviceInfo.thingParamValueAddress())); + params.append(Param(terraAcTcpThingPortParamTypeId, 502)); + params.append(Param(terraAcTcpThingSlaveIdParamTypeId, 1)); + descriptor.setParams(params); + + if (Thing *existingThing = myThings().findByParams(params)) { + descriptor.setThingId(existingThing->id()); + } + + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); + discovery->startDiscovery(); + return; + } + + if (info->thingClassId() == terraAcRtuThingClassId) { + AbbTerraRtuDiscovery *discovery = new AbbTerraRtuDiscovery(hardwareManager()->modbusRtuResource(), info); + connect(discovery, &AbbTerraRtuDiscovery::discoveryFinished, info, [this, info, discovery](bool modbusRtuMasterAvailable) { + if (!modbusRtuMasterAvailable) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("No connected Modbus RTU master available.")); + return; + } + + foreach (const AbbTerraRtuDiscovery::Result &result, discovery->results()) { + ThingDescriptor descriptor(terraAcRtuThingClassId, result.productName, QStringLiteral("Slave ID: %1").arg(result.slaveId)); + ParamList params{ + Param(terraAcRtuThingRtuMasterParamTypeId, result.modbusRtuMasterId), + Param(terraAcRtuThingSlaveIdParamTypeId, result.slaveId) + }; + descriptor.setParams(params); + + if (Thing *existingThing = myThings().findByParams(params)) { + descriptor.setThingId(existingThing->id()); + } + + info->addThingDescriptor(descriptor); + } + + info->finish(Thing::ThingErrorNoError); + }); + discovery->startDiscovery(); + return; + } + + info->finish(Thing::ThingErrorNoError); +} + +void IntegrationPluginAbbterra::setupThing(ThingSetupInfo *info) +{ + if (info->thing()->thingClassId() == terraAcTcpThingClassId) { + setupTcpThing(info); + return; + } + + if (info->thing()->thingClassId() == terraAcRtuThingClassId) { + setupRtuThing(info); + return; + } + + info->finish(Thing::ThingErrorUnsupportedFeature); +} + +void IntegrationPluginAbbterra::postSetupThing(Thing *thing) +{ + Q_UNUSED(thing) + + if (m_pluginTimer) { + return; + } + + m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(30); + connect(m_pluginTimer, &PluginTimer::timeout, this, [this]() { + foreach (AbbTerraModbusTcpConnection *connection, m_tcpConnections) { + connection->update(); + } + foreach (AbbTerraModbusRtuConnection *connection, m_rtuConnections) { + connection->update(); + } + }); + m_pluginTimer->start(); +} + +void IntegrationPluginAbbterra::thingRemoved(Thing *thing) +{ + delete m_tcpConnections.take(thing); + delete m_rtuConnections.take(thing); + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + + if (myThings().isEmpty() && m_pluginTimer) { + hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer); + m_pluginTimer = nullptr; + } +} + +void IntegrationPluginAbbterra::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + + if (thing->thingClassId() == terraAcTcpThingClassId) { + AbbTerraModbusTcpConnection *connection = m_tcpConnections.value(thing); + if (!connection || !connection->reachable()) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The charging station is not reachable.")); + return; + } + + if (info->action().actionTypeId() == terraAcTcpPowerActionTypeId) { + const bool power = info->action().paramValue(terraAcTcpPowerActionPowerParamTypeId).toBool(); + const quint32 currentMilliAmps = power ? static_cast(qRound(thing->stateValue(terraAcTcpMaxChargingCurrentStateTypeId).toDouble() * 1000.0)) : 0; + QModbusReply *reply = connection->setChargingCurrentLimitCommand(currentMilliAmps); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, thing, connection, reply, power]() { + if (reply->error() == QModbusDevice::NoError) { + thing->setStateValue(terraAcTcpPowerStateTypeId, power); + connection->update(); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } + + if (info->action().actionTypeId() == terraAcTcpMaxChargingCurrentActionTypeId) { + const double current = info->action().paramValue(terraAcTcpMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble(); + QModbusReply *reply = connection->setChargingCurrentLimitCommand(static_cast(qRound(current * 1000.0))); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, thing, connection, reply, current]() { + if (reply->error() == QModbusDevice::NoError) { + thing->setStateValue(terraAcTcpMaxChargingCurrentStateTypeId, current); + thing->setStateValue(terraAcTcpPowerStateTypeId, current >= 6.0); + connection->update(); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } + + info->finish(Thing::ThingErrorUnsupportedFeature); + return; + } + + if (thing->thingClassId() == terraAcRtuThingClassId) { + AbbTerraModbusRtuConnection *connection = m_rtuConnections.value(thing); + if (!connection || !connection->reachable()) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The charging station is not reachable.")); + return; + } + + if (info->action().actionTypeId() == terraAcRtuPowerActionTypeId) { + const bool power = info->action().paramValue(terraAcRtuPowerActionPowerParamTypeId).toBool(); + const quint32 currentMilliAmps = power ? static_cast(qRound(thing->stateValue(terraAcRtuMaxChargingCurrentStateTypeId).toDouble() * 1000.0)) : 0; + ModbusRtuReply *reply = connection->setChargingCurrentLimitCommand(currentMilliAmps); + connect(reply, &ModbusRtuReply::finished, info, [info, thing, connection, reply, power]() { + if (reply->error() == ModbusRtuReply::NoError) { + thing->setStateValue(terraAcRtuPowerStateTypeId, power); + connection->update(); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } + + if (info->action().actionTypeId() == terraAcRtuMaxChargingCurrentActionTypeId) { + const double current = info->action().paramValue(terraAcRtuMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toDouble(); + ModbusRtuReply *reply = connection->setChargingCurrentLimitCommand(static_cast(qRound(current * 1000.0))); + connect(reply, &ModbusRtuReply::finished, info, [info, thing, connection, reply, current]() { + if (reply->error() == ModbusRtuReply::NoError) { + thing->setStateValue(terraAcRtuMaxChargingCurrentStateTypeId, current); + thing->setStateValue(terraAcRtuPowerStateTypeId, current >= 6.0); + connection->update(); + info->finish(Thing::ThingErrorNoError); + } else { + info->finish(Thing::ThingErrorHardwareFailure); + } + }); + return; + } + } + + info->finish(Thing::ThingErrorUnsupportedFeature); +} + +void IntegrationPluginAbbterra::setupTcpThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + if (m_tcpConnections.contains(thing)) { + m_tcpConnections.take(thing)->deleteLater(); + } + + NetworkDeviceMonitor *monitor = m_monitors.value(thing); + if (!monitor) { + monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing); + if (!monitor) { + info->finish(Thing::ThingErrorInvalidParameter); + return; + } + m_monitors.insert(thing, monitor); + } + + const quint16 port = static_cast(thing->paramValue(terraAcTcpThingPortParamTypeId).toUInt()); + const quint16 slaveId = static_cast(thing->paramValue(terraAcTcpThingSlaveIdParamTypeId).toUInt()); + AbbTerraModbusTcpConnection *connection = new AbbTerraModbusTcpConnection(monitor->networkDeviceInfo().address(), port, slaveId, thing); + + connect(info, &ThingSetupInfo::aborted, connection, &AbbTerraModbusTcpConnection::deleteLater); + connect(info, &ThingSetupInfo::aborted, monitor, [this, thing]() { + if (m_monitors.contains(thing)) { + hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing)); + } + }); + + connect(monitor, &NetworkDeviceMonitor::networkDeviceInfoChanged, connection, [connection](const NetworkDeviceInfo &networkDeviceInfo) { + connection->modbusTcpMaster()->setHostAddress(networkDeviceInfo.address()); + }); + + connect(connection, &AbbTerraModbusTcpConnection::reachableChanged, thing, [this, thing, connection](bool reachable) { + if (reachable) { + connection->initialize(); + } else { + setDisconnectedState(thing); + } + }); + + connect(connection, &AbbTerraModbusTcpConnection::initializationFinished, thing, [this, thing, connection](bool success) { + if (!success) { + return; + } + + const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromValues(connection->serialNumber(), + connection->firmwareVersionRaw(), + connection->userSettableMaxCurrent()); + if (!deviceInfo.valid) { + return; + } + + thing->setStateValue(terraAcTcpConnectedStateTypeId, true); + thing->setStateValue(terraAcTcpFirmwareVersionStateTypeId, deviceInfo.firmwareVersion); + thing->setStateValue(terraAcTcpSerialNumberStateTypeId, deviceInfo.serialNumber); + thing->setStateMinMaxValues(terraAcTcpMaxChargingCurrentStateTypeId, 6.0, deviceInfo.maxChargingCurrent); + applyTimeoutSetting(thing, connection); + }); + + connect(connection, &AbbTerraModbusTcpConnection::initializationFinished, info, [this, info, thing, connection](bool success) { + if (!success) { + connection->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Could not initialize the communication with the charger.")); + return; + } + + const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromValues(connection->serialNumber(), + connection->firmwareVersionRaw(), + connection->userSettableMaxCurrent()); + if (!deviceInfo.valid) { + connection->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The device does not match the ABB Terra AC Modbus register map.")); + return; + } + + m_tcpConnections.insert(thing, connection); + connection->update(); + info->finish(Thing::ThingErrorNoError); + }); + + connect(connection, &AbbTerraModbusTcpConnection::updateFinished, thing, [this, thing, connection]() { + updateThing(thing, connection); + }); + + connect(thing, &Thing::settingChanged, connection, [this, thing, connection](const ParamTypeId ¶mTypeId, const QVariant &) { + if (paramTypeId == terraAcTcpSettingsCommunicationTimeoutParamTypeId) { + applyTimeoutSetting(thing, connection); + } + }); +} + +void IntegrationPluginAbbterra::setupRtuThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + + if (m_rtuConnections.contains(thing)) { + m_rtuConnections.take(thing)->deleteLater(); + } + + ModbusRtuMaster *master = hardwareManager()->modbusRtuResource()->getModbusRtuMaster(thing->paramValue(terraAcRtuThingRtuMasterParamTypeId).toUuid()); + if (!master) { + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The Modbus RTU connection is not available.")); + return; + } + + const quint16 slaveId = static_cast(thing->paramValue(terraAcRtuThingSlaveIdParamTypeId).toUInt()); + AbbTerraModbusRtuConnection *connection = new AbbTerraModbusRtuConnection(master, slaveId, thing); + + connect(connection, &AbbTerraModbusRtuConnection::reachableChanged, thing, [this, thing, connection](bool reachable) { + if (reachable) { + connection->initialize(); + } else { + setDisconnectedState(thing); + } + }); + + connect(connection, &AbbTerraModbusRtuConnection::initializationFinished, thing, [this, thing, connection](bool success) { + if (!success) { + return; + } + + const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromValues(connection->serialNumber(), + connection->firmwareVersionRaw(), + connection->userSettableMaxCurrent()); + if (!deviceInfo.valid) { + return; + } + + thing->setStateValue(terraAcRtuConnectedStateTypeId, true); + thing->setStateValue(terraAcRtuFirmwareVersionStateTypeId, deviceInfo.firmwareVersion); + thing->setStateValue(terraAcRtuSerialNumberStateTypeId, deviceInfo.serialNumber); + thing->setStateMinMaxValues(terraAcRtuMaxChargingCurrentStateTypeId, 6.0, deviceInfo.maxChargingCurrent); + applyTimeoutSetting(thing, connection); + }); + + connect(connection, &AbbTerraModbusRtuConnection::initializationFinished, info, [this, info, thing, connection](bool success) { + if (!success) { + connection->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Could not initialize the communication with the charger.")); + return; + } + + const AbbTerraUtils::DeviceInfo deviceInfo = AbbTerraUtils::deviceInfoFromValues(connection->serialNumber(), + connection->firmwareVersionRaw(), + connection->userSettableMaxCurrent()); + if (!deviceInfo.valid) { + connection->deleteLater(); + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The device does not match the ABB Terra AC Modbus register map.")); + return; + } + + m_rtuConnections.insert(thing, connection); + connection->update(); + info->finish(Thing::ThingErrorNoError); + }); + + connect(connection, &AbbTerraModbusRtuConnection::updateFinished, thing, [this, thing, connection]() { + updateThing(thing, connection); + }); + + connect(thing, &Thing::settingChanged, connection, [this, thing, connection](const ParamTypeId ¶mTypeId, const QVariant &) { + if (paramTypeId == terraAcRtuSettingsCommunicationTimeoutParamTypeId) { + applyTimeoutSetting(thing, connection); + } + }); +} + +void IntegrationPluginAbbterra::applyTimeoutSetting(Thing *thing, AbbTerraModbusTcpConnection *connection) +{ + QModbusReply *reply = connection->setCommunicationTimeoutCommand(static_cast(thing->setting(terraAcTcpSettingsCommunicationTimeoutParamTypeId).toUInt())); + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, connection, [connection, reply]() { + if (reply->error() != QModbusDevice::NoError && connection->reachable()) { + connection->updateCommunicationTimeoutReadback(); + } + }); +} + +void IntegrationPluginAbbterra::applyTimeoutSetting(Thing *thing, AbbTerraModbusRtuConnection *connection) +{ + ModbusRtuReply *reply = connection->setCommunicationTimeoutCommand(static_cast(thing->setting(terraAcRtuSettingsCommunicationTimeoutParamTypeId).toUInt())); + connect(reply, &ModbusRtuReply::finished, connection, [connection, reply]() { + if (reply->error() != ModbusRtuReply::NoError && connection->reachable()) { + connection->updateCommunicationTimeoutReadback(); + } + }); +} + +void IntegrationPluginAbbterra::updateThing(Thing *thing, AbbTerraModbusTcpConnection *connection) +{ + thing->setStateValue(terraAcTcpConnectedStateTypeId, connection->reachable()); + thing->setStateValue(terraAcTcpPluggedInStateTypeId, AbbTerraUtils::isVehiclePluggedIn(connection->chargingStateRaw(), connection->socketLockState())); + thing->setStateValue(terraAcTcpChargingStateTypeId, AbbTerraUtils::isCharging(connection->chargingStateRaw(), connection->activePower())); + thing->setStateValue(terraAcTcpPowerStateTypeId, connection->chargingCurrentLimit() >= 6000); + thing->setStateValue(terraAcTcpMaxChargingCurrentStateTypeId, connection->chargingCurrentLimit() / 1000.0); + thing->setStateValue(terraAcTcpPhaseCountStateTypeId, AbbTerraUtils::phaseCount(connection->voltageL1(), connection->voltageL2(), connection->voltageL3())); + thing->setStateValue(terraAcTcpCurrentPowerStateTypeId, static_cast(connection->activePower())); + thing->setStateValue("currentPhase1", connection->currentL1() / 1000.0); + thing->setStateValue("currentPhase2", connection->currentL2() / 1000.0); + thing->setStateValue("currentPhase3", connection->currentL3() / 1000.0); + thing->setStateValue("voltagePhase1", connection->voltageL1() / 10.0); + thing->setStateValue("voltagePhase2", connection->voltageL2() / 10.0); + thing->setStateValue("voltagePhase3", connection->voltageL3() / 10.0); + thing->setStateValue(terraAcTcpSessionEnergyStateTypeId, connection->sessionEnergy() / 1000.0); + thing->setStateValue(terraAcTcpErrorCodeStateTypeId, connection->errorCode()); + thing->setSettingValue(terraAcTcpSettingsCommunicationTimeoutParamTypeId, connection->communicationTimeoutReadback()); +} + +void IntegrationPluginAbbterra::updateThing(Thing *thing, AbbTerraModbusRtuConnection *connection) +{ + thing->setStateValue(terraAcRtuConnectedStateTypeId, connection->reachable()); + thing->setStateValue(terraAcRtuPluggedInStateTypeId, AbbTerraUtils::isVehiclePluggedIn(connection->chargingStateRaw(), connection->socketLockState())); + thing->setStateValue(terraAcRtuChargingStateTypeId, AbbTerraUtils::isCharging(connection->chargingStateRaw(), connection->activePower())); + thing->setStateValue(terraAcRtuPowerStateTypeId, connection->chargingCurrentLimit() >= 6000); + thing->setStateValue(terraAcRtuMaxChargingCurrentStateTypeId, connection->chargingCurrentLimit() / 1000.0); + thing->setStateValue(terraAcRtuPhaseCountStateTypeId, AbbTerraUtils::phaseCount(connection->voltageL1(), connection->voltageL2(), connection->voltageL3())); + thing->setStateValue(terraAcRtuCurrentPowerStateTypeId, static_cast(connection->activePower())); + thing->setStateValue("currentPhase1", connection->currentL1() / 1000.0); + thing->setStateValue("currentPhase2", connection->currentL2() / 1000.0); + thing->setStateValue("currentPhase3", connection->currentL3() / 1000.0); + thing->setStateValue("voltagePhase1", connection->voltageL1() / 10.0); + thing->setStateValue("voltagePhase2", connection->voltageL2() / 10.0); + thing->setStateValue("voltagePhase3", connection->voltageL3() / 10.0); + thing->setStateValue(terraAcRtuSessionEnergyStateTypeId, connection->sessionEnergy() / 1000.0); + thing->setStateValue(terraAcRtuErrorCodeStateTypeId, connection->errorCode()); + thing->setSettingValue(terraAcRtuSettingsCommunicationTimeoutParamTypeId, connection->communicationTimeoutReadback()); +} + +void IntegrationPluginAbbterra::setDisconnectedState(Thing *thing) +{ + if (thing->thingClassId() == terraAcTcpThingClassId) { + thing->setStateValue(terraAcTcpConnectedStateTypeId, false); + thing->setStateValue(terraAcTcpChargingStateTypeId, false); + thing->setStateValue(terraAcTcpPluggedInStateTypeId, false); + thing->setStateValue(terraAcTcpCurrentPowerStateTypeId, 0); + thing->setStateValue("currentPhase1", 0); + thing->setStateValue("currentPhase2", 0); + thing->setStateValue("currentPhase3", 0); + thing->setStateValue("voltagePhase1", 0); + thing->setStateValue("voltagePhase2", 0); + thing->setStateValue("voltagePhase3", 0); + return; + } + + if (thing->thingClassId() == terraAcRtuThingClassId) { + thing->setStateValue(terraAcRtuConnectedStateTypeId, false); + thing->setStateValue(terraAcRtuChargingStateTypeId, false); + thing->setStateValue(terraAcRtuPluggedInStateTypeId, false); + thing->setStateValue(terraAcRtuCurrentPowerStateTypeId, 0); + thing->setStateValue("currentPhase1", 0); + thing->setStateValue("currentPhase2", 0); + thing->setStateValue("currentPhase3", 0); + thing->setStateValue("voltagePhase1", 0); + thing->setStateValue("voltagePhase2", 0); + thing->setStateValue("voltagePhase3", 0); + } +} diff --git a/abbterra/integrationpluginabbterra.h b/abbterra/integrationpluginabbterra.h new file mode 100644 index 0000000..84eb1e6 --- /dev/null +++ b/abbterra/integrationpluginabbterra.h @@ -0,0 +1,46 @@ +#ifndef INTEGRATIONPLUGINABBTERRA_H +#define INTEGRATIONPLUGINABBTERRA_H + +#include +#include "integrations/integrationplugin.h" +#include + +#include "extern-plugininfo.h" +#include "abbterramodbusrtuconnection.h" +#include "abbterramodbustcpconnection.h" + +class IntegrationPluginAbbterra : public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginabbterra.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginAbbterra(); + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void thingRemoved(Thing *thing) override; + +public slots: + void executeAction(ThingActionInfo *info) override; + +private: + void setupTcpThing(ThingSetupInfo *info); + void setupRtuThing(ThingSetupInfo *info); + void applyTimeoutSetting(Thing *thing, AbbTerraModbusTcpConnection *connection); + void applyTimeoutSetting(Thing *thing, AbbTerraModbusRtuConnection *connection); + void updateThing(Thing *thing, AbbTerraModbusTcpConnection *connection); + void updateThing(Thing *thing, AbbTerraModbusRtuConnection *connection); + void setDisconnectedState(Thing *thing); + +private: + PluginTimer *m_pluginTimer = nullptr; + QHash m_tcpConnections; + QHash m_rtuConnections; + QHash m_monitors; +}; + +#endif // INTEGRATIONPLUGINABBTERRA_H diff --git a/abbterra/integrationpluginabbterra.json b/abbterra/integrationpluginabbterra.json new file mode 100644 index 0000000..392c218 --- /dev/null +++ b/abbterra/integrationpluginabbterra.json @@ -0,0 +1,416 @@ +{ + "name": "AbbTerra", + "displayName": "ABB Terra AC", + "id": "d7f1cb28-b18b-449e-8cd2-1d99b9d8f681", + "paramTypes": [], + "vendors": [ + { + "id": "0369ebad-5186-437d-b520-041a0b9b7582", + "name": "abb", + "displayName": "ABB", + "thingClasses": [ + { + "id": "93ad828a-9a7a-4fca-be3f-88641317845f", + "name": "terraAcTcp", + "displayName": "Terra AC Charger (TCP)", + "interfaces": [ + "evcharger", + "connectable", + "networkdevice" + ], + "createMethods": [ + "discovery", + "user" + ], + "discoveryType": "weak", + "paramTypes": [ + { + "id": "9b3332aa-6c26-4399-b3dc-d7fdbe3f4420", + "name": "macAddress", + "displayName": "MAC address", + "type": "QString", + "inputType": "MacAddress", + "defaultValue": "", + "readOnly": true + }, + { + "id": "962ef9d9-30a2-4636-8ff2-2a28742277f4", + "name": "address", + "displayName": "Host address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "" + }, + { + "id": "f9e6fed4-2a18-4f23-b70e-78919d4b01f8", + "name": "hostName", + "displayName": "Host name", + "type": "QString", + "inputType": "TextLine", + "defaultValue": "" + }, + { + "id": "153258dd-e81b-48e5-9ba7-46d59e854d85", + "name": "port", + "displayName": "Port", + "type": "uint", + "defaultValue": 502 + }, + { + "id": "cc4c66c1-185f-4c97-88a0-2bc66319d157", + "name": "slaveId", + "displayName": "Slave ID", + "type": "uint", + "defaultValue": 1, + "minValue": 1, + "maxValue": 255 + } + ], + "settingsTypes": [ + { + "id": "5a0e678a-ffc8-443a-9ae7-bb2243b0ec4b", + "name": "communicationTimeout", + "displayName": "Communication timeout", + "type": "uint", + "unit": "Seconds", + "defaultValue": 60, + "minValue": 10, + "maxValue": 65535 + } + ], + "stateTypes": [ + { + "id": "21e1a2bb-0e05-462c-9909-4f9a0eada438", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "2fb30a0b-dc5a-4831-b5c6-de653c0c9ee1", + "name": "pluggedIn", + "displayName": "Plugged in", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "0c685ced-27fa-46ba-8c0b-c49af82054f0", + "name": "charging", + "displayName": "Charging", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "207e2074-0147-4617-9a8b-3f326dcd6a0b", + "name": "power", + "displayName": "Charging enabled", + "displayNameAction": "Set charging enabled", + "type": "bool", + "defaultValue": true, + "writable": true + }, + { + "id": "e3d27f8a-73d0-493a-b99a-29e7dc184485", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameAction": "Set maximum charging current", + "type": "double", + "unit": "Ampere", + "minValue": 6, + "maxValue": 32, + "stepSize": 0.1, + "defaultValue": 6, + "writable": true + }, + { + "id": "0764bce9-fd26-4da8-8d92-f6a5ce73e81e", + "name": "phaseCount", + "displayName": "Phase count", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 1 + }, + { + "id": "59b486a3-aa44-4a94-8478-1077af124441", + "name": "currentPower", + "displayName": "Active power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "953ce672-a9a2-4cd6-bf87-ccfe720c7595", + "name": "currentPhase1", + "displayName": "Current phase 1", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "e4f97cde-191c-4c84-9668-169c5e648338", + "name": "currentPhase2", + "displayName": "Current phase 2", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "c1cead50-9894-4b12-9211-c5bc84109b1d", + "name": "currentPhase3", + "displayName": "Current phase 3", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "bcff4c2d-5567-40ff-adef-5b47dc5ace76", + "name": "voltagePhase1", + "displayName": "Voltage phase 1", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "db7af25a-cb7b-4640-9515-a7af345bd770", + "name": "voltagePhase2", + "displayName": "Voltage phase 2", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "fe67e1bf-6871-45a6-a253-819cb70c5ec4", + "name": "voltagePhase3", + "displayName": "Voltage phase 3", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "be2cc28e-79c2-4eaf-91ad-8131ac1f87e2", + "name": "sessionEnergy", + "displayName": "Session energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "6d9b38a7-c6eb-47bc-8929-5a875d0fd5d0", + "name": "firmwareVersion", + "displayName": "Firmware version", + "type": "QString", + "defaultValue": "" + }, + { + "id": "69c9f4e2-ec8c-4d65-9cf4-ce8b7a3f8c3b", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "" + }, + { + "id": "ce98bfda-b27a-4f69-bd4e-49eb3e722245", + "name": "errorCode", + "displayName": "Error code", + "type": "uint", + "defaultValue": 0 + } + ], + "actionTypes": [] + }, + { + "id": "a385801e-600c-4e27-ad73-5184a7516860", + "name": "terraAcRtu", + "displayName": "Terra AC Charger (RTU)", + "interfaces": [ + "evcharger", + "connectable" + ], + "createMethods": [ + "discovery", + "user" + ], + "paramTypes": [ + { + "id": "deb19a6b-33b3-417e-abf1-a1fb18585fe4", + "name": "rtuMaster", + "displayName": "Modbus RTU master", + "type": "QString", + "defaultValue": "" + }, + { + "id": "23dbb93d-6605-435c-9a41-b8a8e8242ea0", + "name": "slaveId", + "displayName": "Modbus slave ID", + "type": "uint", + "defaultValue": 1, + "minValue": 1, + "maxValue": 247 + } + ], + "settingsTypes": [ + { + "id": "c4e0a515-270e-403e-8453-02002859938e", + "name": "communicationTimeout", + "displayName": "Communication timeout", + "type": "uint", + "unit": "Seconds", + "defaultValue": 60, + "minValue": 10, + "maxValue": 65535 + } + ], + "stateTypes": [ + { + "id": "c43068ca-dddc-4c43-9149-68927afae5e7", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "14fcf045-52d7-4f47-b64e-2bf3f87c242f", + "name": "pluggedIn", + "displayName": "Plugged in", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "64708d42-bbfe-44a5-82ae-c8e66e25c5a5", + "name": "charging", + "displayName": "Charging", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "e35fd4fa-bf5a-45a1-8a39-f0d3d9efa4c6", + "name": "power", + "displayName": "Charging enabled", + "displayNameAction": "Set charging enabled", + "type": "bool", + "defaultValue": true, + "writable": true + }, + { + "id": "ea933a77-a098-4303-bbdb-15c72dfd3634", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameAction": "Set maximum charging current", + "type": "double", + "unit": "Ampere", + "minValue": 6, + "maxValue": 32, + "stepSize": 0.1, + "defaultValue": 6, + "writable": true + }, + { + "id": "cd1add95-18d9-46b5-a3d5-f0f29d5160c9", + "name": "phaseCount", + "displayName": "Phase count", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 1 + }, + { + "id": "152a58cb-0bf1-4d4a-aac2-233d3c68f81b", + "name": "currentPower", + "displayName": "Active power", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "a8e2e2cf-b2ca-48c5-a702-9226062c43e3", + "name": "currentPhase1", + "displayName": "Current phase 1", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "210ff017-ea75-4705-9f23-f73e324601ef", + "name": "currentPhase2", + "displayName": "Current phase 2", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "3a5d6ce5-d3ed-4ba5-996f-15a2b3124faf", + "name": "currentPhase3", + "displayName": "Current phase 3", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "52ebc781-a527-4641-b768-3a0bd0209f96", + "name": "voltagePhase1", + "displayName": "Voltage phase 1", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "bb513653-70ca-4ee1-a913-aab3d2881708", + "name": "voltagePhase2", + "displayName": "Voltage phase 2", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "c84a5e75-121d-4af8-a203-23f586613cf2", + "name": "voltagePhase3", + "displayName": "Voltage phase 3", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "7a9a6ec3-7572-42d8-833d-5b434ca95765", + "name": "sessionEnergy", + "displayName": "Session energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "071d7408-fa96-49da-b56b-f5564bee9b5d", + "name": "firmwareVersion", + "displayName": "Firmware version", + "type": "QString", + "defaultValue": "" + }, + { + "id": "c95ea476-4ce3-4c04-bc23-238bdee496ee", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "" + }, + { + "id": "1f7d2791-c23e-47fe-8512-850ea25e09b3", + "name": "errorCode", + "displayName": "Error code", + "type": "uint", + "defaultValue": 0 + } + ], + "actionTypes": [] + } + ] + } + ] +} diff --git a/abbterra/meta.json b/abbterra/meta.json new file mode 100644 index 0000000..6ebe49a --- /dev/null +++ b/abbterra/meta.json @@ -0,0 +1,13 @@ +{ + "title": "ABB Terra AC", + "tagline": "Connect ABB Terra AC chargers over Modbus TCP or Modbus RTU.", + "stability": "consumer", + "icon": "", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "energy" + ] +} diff --git a/abbterra/translations/d7f1cb28-b18b-449e-8cd2-1d99b9d8f681-en_US.ts b/abbterra/translations/d7f1cb28-b18b-449e-8cd2-1d99b9d8f681-en_US.ts new file mode 100644 index 0000000..7365756 --- /dev/null +++ b/abbterra/translations/d7f1cb28-b18b-449e-8cd2-1d99b9d8f681-en_US.ts @@ -0,0 +1,296 @@ + + + + + AbbTerra + + + ABB + The name of the vendor ({0369ebad-5186-437d-b520-041a0b9b7582}) + + + + + ABB Terra AC + The name of the plugin AbbTerra ({d7f1cb28-b18b-449e-8cd2-1d99b9d8f681}) + + + + + + Active power + The name of the StateType ({152a58cb-0bf1-4d4a-aac2-233d3c68f81b}) of ThingClass terraAcRtu +---------- +The name of the StateType ({59b486a3-aa44-4a94-8478-1077af124441}) of ThingClass terraAcTcp + + + + + + Charging + The name of the StateType ({64708d42-bbfe-44a5-82ae-c8e66e25c5a5}) of ThingClass terraAcRtu +---------- +The name of the StateType ({0c685ced-27fa-46ba-8c0b-c49af82054f0}) of ThingClass terraAcTcp + + + + + + + + Charging enabled + The name of the ParamType (ThingClass: terraAcRtu, ActionType: power, ID: {e35fd4fa-bf5a-45a1-8a39-f0d3d9efa4c6}) +---------- +The name of the StateType ({e35fd4fa-bf5a-45a1-8a39-f0d3d9efa4c6}) of ThingClass terraAcRtu +---------- +The name of the ParamType (ThingClass: terraAcTcp, ActionType: power, ID: {207e2074-0147-4617-9a8b-3f326dcd6a0b}) +---------- +The name of the StateType ({207e2074-0147-4617-9a8b-3f326dcd6a0b}) of ThingClass terraAcTcp + + + + + + Communication timeout + The name of the ParamType (ThingClass: terraAcRtu, Type: settings, ID: {c4e0a515-270e-403e-8453-02002859938e}) +---------- +The name of the ParamType (ThingClass: terraAcTcp, Type: settings, ID: {5a0e678a-ffc8-443a-9ae7-bb2243b0ec4b}) + + + + + + Connected + The name of the StateType ({c43068ca-dddc-4c43-9149-68927afae5e7}) of ThingClass terraAcRtu +---------- +The name of the StateType ({21e1a2bb-0e05-462c-9909-4f9a0eada438}) of ThingClass terraAcTcp + + + + + + Current phase 1 + The name of the StateType ({a8e2e2cf-b2ca-48c5-a702-9226062c43e3}) of ThingClass terraAcRtu +---------- +The name of the StateType ({953ce672-a9a2-4cd6-bf87-ccfe720c7595}) of ThingClass terraAcTcp + + + + + + Current phase 2 + The name of the StateType ({210ff017-ea75-4705-9f23-f73e324601ef}) of ThingClass terraAcRtu +---------- +The name of the StateType ({e4f97cde-191c-4c84-9668-169c5e648338}) of ThingClass terraAcTcp + + + + + + Current phase 3 + The name of the StateType ({3a5d6ce5-d3ed-4ba5-996f-15a2b3124faf}) of ThingClass terraAcRtu +---------- +The name of the StateType ({c1cead50-9894-4b12-9211-c5bc84109b1d}) of ThingClass terraAcTcp + + + + + + Error code + The name of the StateType ({1f7d2791-c23e-47fe-8512-850ea25e09b3}) of ThingClass terraAcRtu +---------- +The name of the StateType ({ce98bfda-b27a-4f69-bd4e-49eb3e722245}) of ThingClass terraAcTcp + + + + + + Firmware version + The name of the StateType ({071d7408-fa96-49da-b56b-f5564bee9b5d}) of ThingClass terraAcRtu +---------- +The name of the StateType ({6d9b38a7-c6eb-47bc-8929-5a875d0fd5d0}) of ThingClass terraAcTcp + + + + + Host address + The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {962ef9d9-30a2-4636-8ff2-2a28742277f4}) + + + + + Host name + The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {f9e6fed4-2a18-4f23-b70e-78919d4b01f8}) + + + + + MAC address + The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {9b3332aa-6c26-4399-b3dc-d7fdbe3f4420}) + + + + + + + + Maximum charging current + The name of the ParamType (ThingClass: terraAcRtu, ActionType: maxChargingCurrent, ID: {ea933a77-a098-4303-bbdb-15c72dfd3634}) +---------- +The name of the StateType ({ea933a77-a098-4303-bbdb-15c72dfd3634}) of ThingClass terraAcRtu +---------- +The name of the ParamType (ThingClass: terraAcTcp, ActionType: maxChargingCurrent, ID: {e3d27f8a-73d0-493a-b99a-29e7dc184485}) +---------- +The name of the StateType ({e3d27f8a-73d0-493a-b99a-29e7dc184485}) of ThingClass terraAcTcp + + + + + Modbus RTU master + The name of the ParamType (ThingClass: terraAcRtu, Type: thing, ID: {deb19a6b-33b3-417e-abf1-a1fb18585fe4}) + + + + + Modbus slave ID + The name of the ParamType (ThingClass: terraAcRtu, Type: thing, ID: {23dbb93d-6605-435c-9a41-b8a8e8242ea0}) + + + + + + Phase count + The name of the StateType ({cd1add95-18d9-46b5-a3d5-f0f29d5160c9}) of ThingClass terraAcRtu +---------- +The name of the StateType ({0764bce9-fd26-4da8-8d92-f6a5ce73e81e}) of ThingClass terraAcTcp + + + + + + Plugged in + The name of the StateType ({14fcf045-52d7-4f47-b64e-2bf3f87c242f}) of ThingClass terraAcRtu +---------- +The name of the StateType ({2fb30a0b-dc5a-4831-b5c6-de653c0c9ee1}) of ThingClass terraAcTcp + + + + + Port + The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {153258dd-e81b-48e5-9ba7-46d59e854d85}) + + + + + + Serial number + The name of the StateType ({c95ea476-4ce3-4c04-bc23-238bdee496ee}) of ThingClass terraAcRtu +---------- +The name of the StateType ({69c9f4e2-ec8c-4d65-9cf4-ce8b7a3f8c3b}) of ThingClass terraAcTcp + + + + + + Session energy + The name of the StateType ({7a9a6ec3-7572-42d8-833d-5b434ca95765}) of ThingClass terraAcRtu +---------- +The name of the StateType ({be2cc28e-79c2-4eaf-91ad-8131ac1f87e2}) of ThingClass terraAcTcp + + + + + + Set charging enabled + The name of the ActionType ({e35fd4fa-bf5a-45a1-8a39-f0d3d9efa4c6}) of ThingClass terraAcRtu +---------- +The name of the ActionType ({207e2074-0147-4617-9a8b-3f326dcd6a0b}) of ThingClass terraAcTcp + + + + + + Set maximum charging current + The name of the ActionType ({ea933a77-a098-4303-bbdb-15c72dfd3634}) of ThingClass terraAcRtu +---------- +The name of the ActionType ({e3d27f8a-73d0-493a-b99a-29e7dc184485}) of ThingClass terraAcTcp + + + + + Slave ID + The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {cc4c66c1-185f-4c97-88a0-2bc66319d157}) + + + + + Terra AC Charger (RTU) + The name of the ThingClass ({a385801e-600c-4e27-ad73-5184a7516860}) + + + + + Terra AC Charger (TCP) + The name of the ThingClass ({93ad828a-9a7a-4fca-be3f-88641317845f}) + + + + + + Voltage phase 1 + The name of the StateType ({52ebc781-a527-4641-b768-3a0bd0209f96}) of ThingClass terraAcRtu +---------- +The name of the StateType ({bcff4c2d-5567-40ff-adef-5b47dc5ace76}) of ThingClass terraAcTcp + + + + + + Voltage phase 2 + The name of the StateType ({bb513653-70ca-4ee1-a913-aab3d2881708}) of ThingClass terraAcRtu +---------- +The name of the StateType ({db7af25a-cb7b-4640-9515-a7af345bd770}) of ThingClass terraAcTcp + + + + + + Voltage phase 3 + The name of the StateType ({c84a5e75-121d-4af8-a203-23f586613cf2}) of ThingClass terraAcRtu +---------- +The name of the StateType ({fe67e1bf-6871-45a6-a253-819cb70c5ec4}) of ThingClass terraAcTcp + + + + + IntegrationPluginAbbterra + + + No connected Modbus RTU master available. + + + + + + The charging station is not reachable. + + + + + + Could not initialize the communication with the charger. + + + + + + The device does not match the ABB Terra AC Modbus register map. + + + + + The Modbus RTU connection is not available. + + + + diff --git a/abbterraac/.claude/REPRISE-ABB-terraac.md b/abbterraac/.claude/REPRISE-ABB-terraac.md new file mode 100644 index 0000000..0a4c7cd --- /dev/null +++ b/abbterraac/.claude/REPRISE-ABB-terraac.md @@ -0,0 +1,153 @@ +# REPRISE — TÂCHE 1 : intégrer & builder ABB Terra AC (vendoring upstream) + +## Contexte +Je prépare une visite client (borne ABB Terra AC + compteur ABB B23). Test EN ATELIER +d'abord. Le plugin **abbterra** se trouve DÉJÀ dans MON repo +`etm-powersync-plugins-modbus` (dossier `abbterra/`) — il a été copié depuis l'upstream +nymea-plugins-modbus, branche `experimental-silo`. Le compteur B2x (TÂCHE 2) est déjà +intégré et prêt (abbb2x ajouté à PLUGIN_DIRS, debian/control + changelog faits). + +But de cette tâche : packager et déployer la borne, proprement, en décidant comment on +gère le fait que ce code est upstream-mais-pas-encore-release. + +## Décision de fond (déjà tranchée — à appliquer, pas à rediscuter) +abbterra N'EST PAS un fork divergent : je ne modifie pas le code, je le **builde en avance** +parce que nymea ne l'a pas encore publié dans son dépôt apt (il n'est que dans la branche +experimental-silo). C'est du **vendoring temporaire**. Tout nymea est GPLv3 → redistribution +et build anticipé explicitement permis, aucune contrainte juridique. + +Approche retenue (cohérente avec l'archi existante du mirror) : +1. **Nom du paquet = `nymea-plugin-abbterra`** (nom UPSTREAM, PAS de préfixe powersync-). +2. **PAS de Provides/Replaces/Conflicts** (rien ne le concurrence : le mirror l'exclura). +3. **Ajouter `abbterra` à `FORKED_PLUGINS`** dans `/mnt/builddisk/sync-nymea-mirror.sh`. + → empêche le mirror de réimporter une version upstream concurrente sous le même nom. + `FORKED_PLUGINS` couvre désormais DEUX cas : forks divergents (keba) ET builds + anticipés de code upstream non encore release (abbterra). Mettre à jour son commentaire. +4. **Tracer le vendoring** : créer `abbterra/VENDORED.md` (voir contenu plus bas). + +Pourquoi ce choix : transition douce. Le jour où nymea release abbterra dans master/stable, +il suffira de (a) supprimer le dossier abbterra/ de mon repo, (b) retirer `abbterra` de +FORKED_PLUGINS, (c) relancer le sync — le mirror tirera alors la version OFFICIELLE sous +le MÊME nom, donc l'edge bascule dessus sans réinstall ni reconfig des things. Un seul nom +de paquet existe à tout instant, jamais de doublon. + +## Infrastructure (rappel) +- VM build : etm-powersync-dev ; conteneur LXC `build-1-15` (libnymea-dev 1.15.0, + libnymea-modbus-dev, qt6-serialport-dev, qt6-serialbus-dev, nymea-dev-tools, python3). +- Edge test : ssh etm@192.168.1.120, nymead actif, canal powersync-testing. +- Dépôt APT : reprepro /mnt/builddisk/apt-repo ; publish-to-repo.sh ; clé GPG ETM. +- Mirror : /mnt/builddisk/sync-nymea-mirror.sh (sélection auto depuis index upstream + moins FORKED_PLUGINS ; keba déjà dedans). +- Repo modbus : git.etm-powersync.fr/ETM-Schurig/etm-powersync-plugins-modbus + local : ~/projects/etm-powersync/etm/etm-powersync-plugins-modbus + - contient déjà : eastron/ (OK, en prod), abbb2x/ (prêt), abbterra/ (à packager). + - modbus.pri recâblé sur paquets système (PKGCONFIG nymea-modbus + modbus-tool.pri). + - Le conteneur CLONE depuis Gitea → TOUJOURS git push avant de builder. + +## Règles de packaging (acquises, à respecter) +- .pro racine : PLUGIN_DIRS une entrée par ligne, PAS de backslash après la dernière ; + pas de SUBDIRS local ni de .depends vers libnymea-modbus (lib système). +- Multi-binaire : le repo aura maintenant 3 paquets (eastron + abbb2x + abbterra) dans + le MÊME debian/ → IL FAUT un `debian/.install` par paquet (nom EXACT du Package:), + sinon dh_install ne route pas les .so → paquet vide. Vérifier que les .install existent + pour les 3 : powersync-plugin-eastron.install, powersync-plugin-abbb2x.install, + nymea-plugin-abbterra.install. Chacun contient la ligne : + usr/lib/*/nymea/plugins/libnymea_integrationplugin.so +- debian/control Build-Depends modbus : debhelper, pkg-config, libnymea-dev, + nymea-dev-tools:native, libnymea-modbus-dev, qt6-base-dev, qt6-base-dev-tools, + qt6-serialport-dev, qt6-serialbus-dev. +- changelog : format strict (ligne vide avant le " --"). Bumper la source en +etm3 + (etm2 = ajout abbb2x déjà fait). +- rules : nettoyer autogenerated/ + moc_* + *plugininfo.h au dh_auto_clean. + +## ÉTAPES + +### 1. Vérifier l'état d'abbterra dans le repo + ls -la ~/projects/.../etm-powersync-plugins-modbus/abbterra/ + grep -n 'PLUGIN_DIRS\|abbterra\|abbb2x\|eastron' etm-powersync-plugins-modbus.pro + - abbterra présent ? abbterra dans PLUGIN_DIRS ? (l'ajouter si absent, sans casser le backslash) + +### 2. debian/ — ajouter le paquet nymea-plugin-abbterra + - debian/control : nouveau stanza `Package: nymea-plugin-abbterra` + Architecture: any + Section: libs + Depends: ${shlibs:Depends}, ${misc:Depends} + (PAS de Provides/Replaces/Conflicts) + Description: ABB Terra AC charging station (Modbus TCP/RTU) — vendored from + nymea-plugins-modbus experimental-silo, pending upstream release. + - debian/nymea-plugin-abbterra.install : + usr/lib/*/nymea/plugins/libnymea_integrationpluginabbterra.so + - debian/changelog : nouvelle entrée en tête, version 1.15.0+etm3 (ligne vide avant " --"). + - Vérifier que les .install des 3 paquets existent (cf. règles multi-binaire). + +### 3. Tracer le vendoring — créer abbterra/VENDORED.md + Contenu : + ---------------------------------------------------------------- + # Vendoring — abbterra + Copié depuis : nymea/nymea-plugins-modbus @ branche experimental-silo + Commit source : a652793 ("Add new plugin for ABB Terra AC Charger") + Date copie : 2026-06-01 + Raison : plugin présent upstream mais PAS encore publié dans le dépôt apt nymea. + Build anticipé ETM en attendant la release master/stable. + Nom paquet : nymea-plugin-abbterra (nom upstream conservé) + Exclu du mirror via FORKED_PLUGINS dans sync-nymea-mirror.sh. + SORTIE (quand nymea release abbterra dans master/stable) : + 1) supprimer le dossier abbterra/ de ce repo + 2) retirer "abbterra" de FORKED_PLUGINS + 3) relancer sync-nymea-mirror.sh → le mirror tire la version officielle (même nom) + ---------------------------------------------------------------- + +### 4. Mirror — exclure abbterra + Dans /mnt/builddisk/sync-nymea-mirror.sh, dans FORKED_PLUGINS : + FORKED_PLUGINS=( + "keba" + "abbterra" + ) + Mettre à jour le commentaire au-dessus pour préciser les 2 cas (fork divergent + vendoring). + +### 5. Build (pipeline validé) + git add -A && git commit -m "..." && git push + sudo lxc exec build-1-15 -- bash -c ' + set -e + cd /root && rm -rf etm-powersync-plugins-modbus + git clone https://git.etm-powersync.fr/ETM-Schurig/etm-powersync-plugins-modbus.git + cd etm-powersync-plugins-modbus + echo "=== plugins ===" && grep -A6 PLUGIN_DIRS etm-powersync-plugins-modbus.pro + echo "=== installs ===" && ls debian/*.install + echo "=== packages ===" && grep -c "^Package:" debian/control + chmod +x debian/rules + # build local d abord pour valider compil + generation, PUIS le .deb : + qmake6 && make -j$(nproc) 2>&1 | tail -30 + ' + - Vérifier dans la sortie : abbterra ET abbb2x ET eastron compilent, .so produites. + - Si abbterra casse sur des getters/connexion : c est du code upstream testé, donc + plutot un souci d intégration (PLUGIN_DIRS, modbus.pri) qu un bug — lire l erreur. + - Puis : dpkg-buildpackage -b -us -uc 2>&1 | tail -30 + +### 6. Vérifier les .deb (anti-paquet-vide) + Pour chacun des 3 : dpkg-deb -c | grep '\.so' → une .so au bon chemin. + Pour abbterra : dpkg-deb -f nymea-plugin-abbterra_*.deb Package Depends + (Depends doit inclure libqt6serialbus/serialport via shlibs). + +### 7. Import + déploiement + lxc file pull des .deb vers /mnt/builddisk + reprepro -b /mnt/builddisk/apt-repo includedeb powersync-testing (les 3 + dbgsym) + /mnt/builddisk/publish-to-repo.sh + reprepro -b /mnt/builddisk/apt-repo list powersync-testing | grep -iE 'abbterra|abbb2x' + ssh etm@192.168.1.120 'sudo apt update && sudo apt install -y nymea-plugin-abbterra powersync-plugin-abbb2x && sudo systemctl restart nymead' + +## Config en atelier (après install) +- Borne ABB Terra : ThingClass terraAcTcp (address, port 502, slaveId 1) OU terraAcRtu + (rtuMaster, slaveId 1). CONFIRMER sur le matériel : TCP (réseau) ou RTU (RS-485) ? +- Compteur ABB B2x : RTU (rtuMaster + slaveAddress). Si borne RTU + compteur RTU sur le + MÊME bus → slaveId DISTINCTS. +- RTU : créer d'abord le "Modbus RTU master" (adaptateur /dev/ttyUSB*) dans nymea:app, + vérifier droits (user nymead dans groupe dialout), puis ajouter les things. +- B2x = code NEUF : valider scaling (puissance signée +import/-export ; si ×100 trop grand + passer /100 à /1 dans le .cpp ; vérifier noms getters générés dans + abbb2x/autogenerated/abbb2xmodbusrtuconnection.h). + +## Préférences de travail +Français. Affichage complet des fichiers (cat -n) plutôt que diffs ; logs complets avant +commit ; pas de fallback silencieux. Étape par étape : attendre ma sortie de commande avant +de continuer. Toujours vérifier le contenu d'un .deb (dpkg-deb -c) avant import. diff --git a/debian/changelog b/debian/changelog index 9bf706a..5056cad 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,20 @@ +etm-powersync-plugins-modbus (1.15.0+etm3) trixie; urgency=medium + + * Ajout plugin ABB Terra AC (borne de charge, Modbus TCP/RTU) : vendoring + depuis nymea-plugins-modbus experimental-silo (commit a652793), en + attente de la release master/stable upstream. Nom paquet nymea-plugin-abbterra + (nom upstream conserve pour transition sans reinstall). + + -- ETM-Schurig SARL Sun, 01 Jun 2026 10:00:00 +0200 + +etm-powersync-plugins-modbus (1.15.0+etm2) trixie; urgency=medium + + * Ajout plugin ABB B2x (B21/B23/B24, Modbus RTU) : tensions, courants, + puissances par phase (int32 signe pour injection PV), frequence, + energie import/export (uint64 x4 registres, 0.01 kWh). + + -- ETM-Schurig SARL Sun, 01 Jun 2026 09:00:00 +0200 + etm-powersync-plugins-modbus (1.15.0+etm1) trixie; urgency=medium * Initial ETM packaging of the eastron plugin (Eastron SDM72/120/630, Modbus RTU). diff --git a/debian/control b/debian/control index 098b0b6..716f94e 100644 --- a/debian/control +++ b/debian/control @@ -23,3 +23,21 @@ Depends: ${shlibs:Depends}, Description: PowerSync integration plugin for Eastron SDM energy meters (Modbus RTU) nymea integration plugin for Eastron SDM72 / SDM120 / SDM630 energy meters over Modbus RTU, for use by the ETM PowerSync home energy management system. + +Package: powersync-plugin-abbb2x +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: PowerSync integration plugin for ABB B2x energy meters (Modbus RTU) + nymea integration plugin for ABB B21 / B23 / B24 energy meters + over Modbus RTU, for use by the ETM PowerSync home energy management system. + +Package: nymea-plugin-abbterra +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: ABB Terra AC charging station (Modbus TCP/RTU) - vendored from upstream + nymea integration plugin for ABB Terra AC wallbox over Modbus TCP or RTU. + Vendored from nymea-plugins-modbus experimental-silo, pending upstream release. diff --git a/debian/nymea-plugin-abbterra.install b/debian/nymea-plugin-abbterra.install new file mode 100644 index 0000000..5683d21 --- /dev/null +++ b/debian/nymea-plugin-abbterra.install @@ -0,0 +1 @@ +usr/lib/*/nymea/plugins/libnymea_integrationpluginabbterra.so diff --git a/debian/powersync-plugin-abbb2x.install b/debian/powersync-plugin-abbb2x.install new file mode 100644 index 0000000..36858a9 --- /dev/null +++ b/debian/powersync-plugin-abbb2x.install @@ -0,0 +1 @@ +usr/lib/*/nymea/plugins/libnymea_integrationpluginabbb2x.so diff --git a/debian/powersync-plugin-eastron.install b/debian/powersync-plugin-eastron.install new file mode 100644 index 0000000..595c118 --- /dev/null +++ b/debian/powersync-plugin-eastron.install @@ -0,0 +1 @@ +usr/lib/*/nymea/plugins/libnymea_integrationplugineastron.so diff --git a/etm-powersync-plugins-modbus.pro b/etm-powersync-plugins-modbus.pro index a7d0b8d..267edd0 100644 --- a/etm-powersync-plugins-modbus.pro +++ b/etm-powersync-plugins-modbus.pro @@ -4,7 +4,9 @@ TEMPLATE = subdirs # dependency on the libs will be defined PLUGIN_DIRS = \ - eastron + eastron \ + abbb2x \ + abbterra message(============================================)