Ajout plugin ABB Terra AC (vendoring upstream experimental-silo)

- abbterra/ copié depuis nymea-plugins-modbus experimental-silo (a652793)
- paquet nymea-plugin-abbterra (nom upstream, vendoring tracé dans VENDORED.md)
- 3 paquets binaires => .install par paquet
- abbterra ajouté à FORKED_PLUGINS du mirror (exclusion upstream)
- changelog 1.15.0+etm3
This commit is contained in:
Patrick Schurig 2026-06-01 07:38:51 +02:00
parent da2ce5253d
commit c26b0336bd
24 changed files with 2428 additions and 1 deletions

View File

@ -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 <repo> && git clone <url> && cd <repo>
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.

View File

@ -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/<paquet>.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<nom>.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 <deb> | 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 <deb> (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.

19
abbterra/README.md Normal file
View File

@ -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`

12
abbterra/VENDORED.md Normal file
View File

@ -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)

View File

@ -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"
}
]
}

20
abbterra/abbterra.pro Normal file
View File

@ -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

View File

@ -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::Result> 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();
});
}

View File

@ -0,0 +1,41 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef ABBTERRARTUDISCOVERY_H
#define ABBTERRARTUDISCOVERY_H
#include <QObject>
#include <hardware/modbus/modbusrtuhardwareresource.h>
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<Result> results() const;
signals:
void discoveryFinished(bool modbusRtuMasterAvailable);
private:
void scanNext();
private:
ModbusRtuHardwareResource *m_modbusRtuResource = nullptr;
QList<ModbusRtuMaster *> m_candidateMasters;
QList<Result> m_results;
int m_masterIndex = 0;
quint16 m_slaveId = 1;
};
#endif // ABBTERRARTUDISCOVERY_H

View File

@ -0,0 +1,127 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "abbterratcpdiscovery.h"
#include "abbterrautils.h"
#include "extern-plugininfo.h"
#include <QTime>
#include <QTimer>
#include <utility>
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::Result> 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<AbbTerraModbusTcpConnection *> 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();
}

View File

@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef ABBTERRATCPDISCOVERY_H
#define ABBTERRATCPDISCOVERY_H
#include <QObject>
#include <QDateTime>
#include <network/networkdevicediscovery.h>
#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<Result> 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<AbbTerraModbusTcpConnection *> m_connections;
QList<Result> m_results;
QList<Result> m_temporaryResults;
};
#endif // ABBTERRATCPDISCOVERY_H

145
abbterra/abbterrautils.h Normal file
View File

@ -0,0 +1,145 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef ABBTERRAUTILS_H
#define ABBTERRAUTILS_H
#include <QString>
#include <QVector>
#include <modbusdatautils.h>
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<quint8>((serialNumberRaw >> 56) & 0xff);
const quint8 ratedPowerCode = static_cast<quint8>((serialNumberRaw >> 48) & 0xff);
const quint8 plantId = static_cast<quint8>((serialNumberRaw >> 40) & 0xff);
const quint8 productionWeek = static_cast<quint8>((serialNumberRaw >> 24) & 0xff);
const quint8 productionYear = static_cast<quint8>((serialNumberRaw >> 16) & 0xff);
const quint8 uniqueHigh = static_cast<quint8>((serialNumberRaw >> 8) & 0xff);
const quint8 uniqueLow = static_cast<quint8>(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<char>(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<quint16> &registers)
{
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<quint8>((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

View File

@ -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 <QLoggingCategory>
#include <QtMath>
#include <utility>
#include <hardwaremanager.h>
#include <hardware/modbus/modbusrtuhardwareresource.h>
#include <network/networkdevicediscovery.h>
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<quint32>(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<quint32>(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<quint32>(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<quint32>(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<quint16>(thing->paramValue(terraAcTcpThingPortParamTypeId).toUInt());
const quint16 slaveId = static_cast<quint16>(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 &paramTypeId, 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<quint16>(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 &paramTypeId, const QVariant &) {
if (paramTypeId == terraAcRtuSettingsCommunicationTimeoutParamTypeId) {
applyTimeoutSetting(thing, connection);
}
});
}
void IntegrationPluginAbbterra::applyTimeoutSetting(Thing *thing, AbbTerraModbusTcpConnection *connection)
{
QModbusReply *reply = connection->setCommunicationTimeoutCommand(static_cast<quint16>(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<quint16>(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<double>(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<double>(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);
}
}

View File

@ -0,0 +1,46 @@
#ifndef INTEGRATIONPLUGINABBTERRA_H
#define INTEGRATIONPLUGINABBTERRA_H
#include <plugintimer.h>
#include "integrations/integrationplugin.h"
#include <network/networkdevicemonitor.h>
#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<Thing *, AbbTerraModbusTcpConnection *> m_tcpConnections;
QHash<Thing *, AbbTerraModbusRtuConnection *> m_rtuConnections;
QHash<Thing *, NetworkDeviceMonitor *> m_monitors;
};
#endif // INTEGRATIONPLUGINABBTERRA_H

View File

@ -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": []
}
]
}
]
}

13
abbterra/meta.json Normal file
View File

@ -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"
]
}

View File

@ -0,0 +1,296 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>AbbTerra</name>
<message>
<location filename="../plugininfo.h" line="80"/>
<source>ABB</source>
<extracomment>The name of the vendor ({0369ebad-5186-437d-b520-041a0b9b7582})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="83"/>
<source>ABB Terra AC</source>
<extracomment>The name of the plugin AbbTerra ({d7f1cb28-b18b-449e-8cd2-1d99b9d8f681})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="86"/>
<location filename="../plugininfo.h" line="89"/>
<source>Active power</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="92"/>
<location filename="../plugininfo.h" line="95"/>
<source>Charging</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="98"/>
<location filename="../plugininfo.h" line="101"/>
<location filename="../plugininfo.h" line="104"/>
<location filename="../plugininfo.h" line="107"/>
<source>Charging enabled</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="110"/>
<location filename="../plugininfo.h" line="113"/>
<source>Communication timeout</source>
<extracomment>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})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="116"/>
<location filename="../plugininfo.h" line="119"/>
<source>Connected</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="122"/>
<location filename="../plugininfo.h" line="125"/>
<source>Current phase 1</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="128"/>
<location filename="../plugininfo.h" line="131"/>
<source>Current phase 2</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="134"/>
<location filename="../plugininfo.h" line="137"/>
<source>Current phase 3</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="140"/>
<location filename="../plugininfo.h" line="143"/>
<source>Error code</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="146"/>
<location filename="../plugininfo.h" line="149"/>
<source>Firmware version</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="152"/>
<source>Host address</source>
<extracomment>The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {962ef9d9-30a2-4636-8ff2-2a28742277f4})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="155"/>
<source>Host name</source>
<extracomment>The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {f9e6fed4-2a18-4f23-b70e-78919d4b01f8})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="158"/>
<source>MAC address</source>
<extracomment>The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {9b3332aa-6c26-4399-b3dc-d7fdbe3f4420})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="161"/>
<location filename="../plugininfo.h" line="164"/>
<location filename="../plugininfo.h" line="167"/>
<location filename="../plugininfo.h" line="170"/>
<source>Maximum charging current</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="173"/>
<source>Modbus RTU master</source>
<extracomment>The name of the ParamType (ThingClass: terraAcRtu, Type: thing, ID: {deb19a6b-33b3-417e-abf1-a1fb18585fe4})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="176"/>
<source>Modbus slave ID</source>
<extracomment>The name of the ParamType (ThingClass: terraAcRtu, Type: thing, ID: {23dbb93d-6605-435c-9a41-b8a8e8242ea0})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="179"/>
<location filename="../plugininfo.h" line="182"/>
<source>Phase count</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="185"/>
<location filename="../plugininfo.h" line="188"/>
<source>Plugged in</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="191"/>
<source>Port</source>
<extracomment>The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {153258dd-e81b-48e5-9ba7-46d59e854d85})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="194"/>
<location filename="../plugininfo.h" line="197"/>
<source>Serial number</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="200"/>
<location filename="../plugininfo.h" line="203"/>
<source>Session energy</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="206"/>
<location filename="../plugininfo.h" line="209"/>
<source>Set charging enabled</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="212"/>
<location filename="../plugininfo.h" line="215"/>
<source>Set maximum charging current</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="218"/>
<source>Slave ID</source>
<extracomment>The name of the ParamType (ThingClass: terraAcTcp, Type: thing, ID: {cc4c66c1-185f-4c97-88a0-2bc66319d157})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="221"/>
<source>Terra AC Charger (RTU)</source>
<extracomment>The name of the ThingClass ({a385801e-600c-4e27-ad73-5184a7516860})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="224"/>
<source>Terra AC Charger (TCP)</source>
<extracomment>The name of the ThingClass ({93ad828a-9a7a-4fca-be3f-88641317845f})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="227"/>
<location filename="../plugininfo.h" line="230"/>
<source>Voltage phase 1</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="233"/>
<location filename="../plugininfo.h" line="236"/>
<source>Voltage phase 2</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../plugininfo.h" line="239"/>
<location filename="../plugininfo.h" line="242"/>
<source>Voltage phase 3</source>
<extracomment>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</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>IntegrationPluginAbbterra</name>
<message>
<location filename="../integrationpluginabbterra.cpp" line="57"/>
<source>No connected Modbus RTU master available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginabbterra.cpp" line="141"/>
<location filename="../integrationpluginabbterra.cpp" line="186"/>
<source>The charging station is not reachable.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginabbterra.cpp" line="289"/>
<location filename="../integrationpluginabbterra.cpp" line="365"/>
<source>Could not initialize the communication with the charger.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginabbterra.cpp" line="298"/>
<location filename="../integrationpluginabbterra.cpp" line="374"/>
<source>The device does not match the ABB Terra AC Modbus register map.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginabbterra.cpp" line="328"/>
<source>The Modbus RTU connection is not available.</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@ -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/<paquet>.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<nom>.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 <deb> | 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 <deb> (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.

17
debian/changelog vendored
View File

@ -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 <contact@etm-schurig.eu> 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 <contact@etm-schurig.eu> 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).

18
debian/control vendored
View File

@ -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.

1
debian/nymea-plugin-abbterra.install vendored Normal file
View File

@ -0,0 +1 @@
usr/lib/*/nymea/plugins/libnymea_integrationpluginabbterra.so

View File

@ -0,0 +1 @@
usr/lib/*/nymea/plugins/libnymea_integrationpluginabbb2x.so

View File

@ -0,0 +1 @@
usr/lib/*/nymea/plugins/libnymea_integrationplugineastron.so

View File

@ -4,7 +4,9 @@ TEMPLATE = subdirs
# dependency on the libs will be defined
PLUGIN_DIRS = \
eastron
eastron \
abbb2x \
abbterra
message(============================================)