From a72f421db51b8a47c6b7a8d9753897966d531ab6 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 17 Nov 2022 11:16:40 +0100 Subject: [PATCH] New plugin: EVBox --- debian/control | 9 + debian/nymea-plugin-evbox.install.in | 2 + evbox/README.md | 17 + evbox/evbox-logo-blue.png | Bin 0 -> 7770 bytes evbox/evbox.pro | 9 + evbox/integrationpluginevbox.cpp | 348 ++++++++++++++++++ evbox/integrationpluginevbox.h | 83 +++++ evbox/integrationpluginevbox.json | 97 +++++ evbox/meta.json | 13 + ...2ac5c-5e2f-43c0-b3fc-70a98773e119-en_US.ts | 101 +++++ nymea-plugins.pro | 1 + 11 files changed, 680 insertions(+) create mode 100644 debian/nymea-plugin-evbox.install.in create mode 100644 evbox/README.md create mode 100644 evbox/evbox-logo-blue.png create mode 100644 evbox/evbox.pro create mode 100644 evbox/integrationpluginevbox.cpp create mode 100644 evbox/integrationpluginevbox.h create mode 100644 evbox/integrationpluginevbox.json create mode 100644 evbox/meta.json create mode 100644 evbox/translations/3362ac5c-5e2f-43c0-b3fc-70a98773e119-en_US.ts diff --git a/debian/control b/debian/control index 3fdbb21d..5b7039f0 100644 --- a/debian/control +++ b/debian/control @@ -219,6 +219,15 @@ Description: nymea integration plugin for ESPuino This package contains the nymea integration plugin for ESPuino devices. +Package: nymea-plugin-evbox +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, +Description: nymea integration plugin for EVBox + This package contains the nymea integration plugin for EVBox wallboxes + implementing the Protocol Max v4. + + Package: nymea-plugin-fastcom Architecture: any Depends: ${misc:Depends}, diff --git a/debian/nymea-plugin-evbox.install.in b/debian/nymea-plugin-evbox.install.in new file mode 100644 index 00000000..1973c152 --- /dev/null +++ b/debian/nymea-plugin-evbox.install.in @@ -0,0 +1,2 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginevbox.so +evbox/translations/*qm usr/share/nymea/translations/ diff --git a/evbox/README.md b/evbox/README.md new file mode 100644 index 00000000..fed95b89 --- /dev/null +++ b/evbox/README.md @@ -0,0 +1,17 @@ +# EVBox + +This integration allows nymea to control EVBox wallboxes supporting the Protocol Max v4. + +## Supported things + +Generally, all EVBox wallboxes supporting the Protocol Max v4 are supported. We've tested it with + +* Elvi + +## Requirements + +The EVBox Protocol Max v4 is based on a RS485 connection. This means, a RS485 port, either onboard or via USB adapter, is required on the nymea system. +The wallbox must be configured to not be cloud controlled, in order to accept commands on the RS485 port. + + + diff --git a/evbox/evbox-logo-blue.png b/evbox/evbox-logo-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..0ba3785e4dff17bd774ab135afe4c0639cd3d0d3 GIT binary patch literal 7770 zcmd6M^-~*8&^HMLLMX*u3lw*EDelD`(&B~UZb>PH;)No?r9cHQ4#nNwAq00V#pU7q z%=-_#zdbiMb2B@i-ML$v-Mz@yTFST(N(dSn8m_8}f-V{wdK(%VKnaZgL`e+6RD3!F zUTf(8cdGQA{(o(gbk&6?HheE#Ej=`RdS2PrP&asdPI>F-$}uBlXcQV6O^>RAoW9?} zVb)^kH+FnQ5&|8azmHadv^QSbg66;K!KxPlS-VUO84LREyW&r>{_n1x#6{wE17?a_M-%ONHAJyGhXrKw{v1is;=4)vi*DB5M#2nmY$`|{eAd*?tH!)T9_5u z3Z68}6?ZQqPDWJfu}9_RyhVff_LNB1n-c<+zkR)VikXd;5npOX_p&rsdC+pBe35i% zCJan}jMx2@KHw_@h&@L6&)A#-Tam11(%@}*?vVo(;u(oKGA8kwr;%ijk^m(vD{(5$~5dSeV z;UYGey|?OJ{K_?$=-y8x&N-fzQA+Wct|8mEzx2%^f>?`y;>WnoC;n=>(m&eStL>ZL z&cOhHXTJf{fj|uHAeZ(ZZ86W^GyONTCcwQ$mueAUzw&MG*tw(Ian>?9xE?cYOX*=e z=VE#AeXaaTD_&z#+0^{Z4>HC%8WIBRiE=!^itX<92EX_Q_oWq)>U@h zJ1zBFo@4VwHuJ1TVr|8Mfi2OThqRS99Zd0P{arlLuQv&;N2|0tO@FJ9m#)KX{N7V1 z8hejXWCwn}vP>2S>JPQIcJ(A9+9}ST3@0Q++;YHmmh7q7o+<^B3HS)#|MG2>k-u*{ zSt5U?vp1|Db@y0n(z@K&r|Zc6Mgg(*mf=1{GzvEy-;JGO;?eBgiC+ArjvFj^A?<=< zecqP3OR|2cdFE4^YK*yy9QepM(jfZWc8Bj&Zzc!W%XXFC!Vqp1t^T5?B$fW)rm1Ca zF&j)5Q!{qFpU3i$dUQb0LOg`w3zqvt-E%`AQ>R^Tw0Lzs24qnqT;p|oc*C$3; zPJRqo-u_f$X`hZE2BMd^!B(meJNb^k2qfa%sFy~(#W~cZU)~?m~fj%`R0pG z17M5%e!-4bySK4?BBwpTU1k)^X+Ex_F{wP|Dpa!AO;oyt1+BFhKKbs%I-I9u8JOh1 zY9d?9e`E{GBUo+NUA;2PVB%J;E#AQ&QvLH)`cw~i(gYEGuXmF8Vxv`#?J$4k!xx+@ zk_PZ-tJPm{5uQ*@)bsY3>dhNXz?B6kb1Uz95OH}ysdd`JmJwpV-8%q1sM`e8<%V*} z|QapSz)nA0Be$msbqh*fK(|p9e%c z)^i07RnM4a%&F4|2LKlu3rGFlW<68j8Fp_}r@v#!967i}|d%C^9K1!2aHzkLJjYGcez5mYV*k{3S0FN@iS&_EQ zI6@x^{eE8cTd`dVuhI&nkJ%_kW;nGNKL?O8FJPY1>@O@cpvv|eKjf``++iZ|%x8;#d(Cg3D(Y1c#f^`{=oKDTSX(1AHyCTWe?2)TL;YD=TgrFLRP*q>E2t zg5rj)KY8?4M@_L%?D&$pl@P4C+K4tvfDT%kKOg03)@SL{uQ~dN!^!*u5LJDGxcZ`k+>OD(@SBG=; zkCB0pwtghzSeuwaLQb;OoMmzasq3%lAMn)7RCy{2)1i*)p(HSQT1EW(eJg-uZIU;t7ZlK8fA`#x*F z2`tme{{!!31@JJTAF6*n#QvxbT`ap8(Qx4Qi}>K8+s4;xb0T&A^2mfb&!u1$k>UTCaTzK#mSNFT9J+nCLQ5bYxY-_c=QPx z&?%p#d-Laz$iHLxl5(dAgF2yS%>}aM3d&}O_-(XCPgAJ!U@43=Z_WtJ3_E_kckM3h z@@fFg;B97dOkhgSON*&)WiVcBjElYX)9Z=p>h2muC5SAwj- z!BV^UUS@M{|HusCKLrEf$3kP8*rHoKd_@s`U7Q&Mj(K6$h)N39 z1>^%ZW}_Tnu!Hud@9KvL*bf@=$sqR;oMo}4Hx{FeY!f6T%jEpfqQF1S@yzQZFpd=@5DE@t{8+|;a%0tv>#HSbYL*w{)8&^}Qc4q#QW#-E(!YFlNyMUme#8Q}RQ18VkVF2>idkWaqCg8EFf zy@R!1@f2Ma7JvvW>R#ANf5YW!QOibV35S;Ot?lcp0QBWdIHOwo=3T=t4%7z`KV0s? zAIAZy=e451b)NSnG|=1(BDbzckqyoEgQp~A0T}&LCe~B}`v}aIkJYH^;T=1~Mp!|L zTp*Pl5YyU7e5zHc)Dq8-H{|HfQud=F_%e^6b3sUwSu+EoRmK9-zhOi-O*$`PPqy`|3 ziZ2U%(rf{|dj2ak@7XptTSx9z(J&j|0F;wLRf)u0e4O{+K@iZ~EGkTpzlJ1#tGMPd zzj}S15&2FX@riMdJY8DvQGRoTM_rna!hEcBj()?+vew9U{OUW4uA_-^fiW(~lM;|> z&v1ej0ea_w4f&H-;R3xu{IDQOR+)eAu6u*zuV*QCSOKog95HNW8(uAc)p2zJt7mj~(OFgcVGmvYgH{U%lrur|kRpr}Mv*uk_9*j~`8Ou)fP>9PXA3j1}|+6Jsf*u0xE1&4Fw@DZ7TCe99}T<(W5+oTeEn;POjM`Z86Ft z`U2E9;Lrh8F0yFbap9z?``<4fzOmS+XMKBt!q!Bd=DKv6WE)_58tV=~R84-8SsO}S zAURX|(?agJ(U*V0=L?~a$mN!{ljX#a9P04H0hxcTW>Pa|L{*P2@+Oq~qA;mORMz6fK&WrJF&?J0*5$z zGko5rI~M*;*%?eKmwZzVd8dxw-38gg;|X5FmExUffI50;Lg`ll>xOrgt3F&;z?OYM zH6?60Wo*QD-ZPA)Q26{HT^;wJifm14UENn54Zw;ItzhRF;>p2H?jaaK@o>*9^ueXR zit0tTCJsCx4^uZ??1T5o$CO=HX>AE)iRAu?hE;LvEAju})wIC7} zC|Gi3eu3HpZXI>O{*B3dfL&`>Qov#uSMfJ=@ zcej`rOWqbDoaZeP7MZ^{)gPC?RE%D_R8zsIyK7mGpFg`5TQUnrRmio~c%m<^4H*X}LVrrzlxLIRO(C{_;y&K^+?a8*9Td)(#v{x5k(_-g z%56EVb*=sY&xkeYnUK*~Y(#pvkiC$f#yam~-&?DJqiG%3-s1OcXsHbw`aLdl%^_%r6RRv?a=X+? zyG;<3F$r6W)JxEP(riq69M9SP+MH$^g~wNXw})%}l86+=(iy2qp2TG0?~&m`YsN0B z>{SedYXP-LncR|}{_!RKs$fwGr;Xe6b+45qxauJG_SePNQ~oO0ATxU|Ly+qcHL3fC z6lQw&%|Y|MD?QYX?aGy~o=nJ81RL?2)K&&N_{I0Lx&x^|hHb1*O)@&T=L&}u92n*X zZ9_D;%KA5^LhMjoW*5JUg%s6_KaQmpo{!Dv!$*uXsd=uxU+~?gw-DbXtK)Qn1A(N1 zcwE7|Uh<}OhHgDBlIw|)0_G99<*5%F&G*Oj(5=KRvHZJgK@O;z?1I);RHXFxA?nb0 zEdK&g^VSamQ6pV!Iz#ww2J#&opksnxbPFWBg-KH~`$6BDPNepo63?k0cbo&lKiGNU zVgqKKpKXn^%lC589a}U3j~vSt8akJ=JyB7`{-q;)o1X)x(Rn6M8<=mnFqV}491Kq# z-x;?h(SOs9v(B%xoT1!RqkDnZyYZpuA^CE{(XyHo+R@3wlI3b+ zpHXK;#3Ljjv6FB|aAE5?l|P9FFe2uyBIZPt1s16oT;R%U%$O`b!Vgk#I z_13cpO19SUrc&VD^9Yxz+QFMR%rz#(g<)y$!+q*SF=lXmHau;6(tT?v@bUqeLWaZP zn85d!ANCz2?F#0bL73ui6YeZ;- zeMmWFBSZ(xnx)hd6IEOh17x=MK!q^-<1wn2Xgr4YG_JW6S) zqRr3#W!=wVn6;_n`_y4cx|pQG?8<1;9Wn{^%!#azcuR|q!oZ`8S3tv39OJJm{Z&TN zpP;w%Br>Wrd9ba5-d}UeW)GG(ZYAFWD@@D{_Z-joS$w3|R?6|+gaG^eH?cHH$3!P; zIhYPcMh4s#k=xobe!KA1r2G4ZW{pqh93`6C|BObW=!8P!pXMrnXnvq&;|xe+H@$Uv z?mFGlh3zh~br$1aPsPMn?*MVyhqc&BAt{59>~X`8pOGPS08hNSz$BVPS0Y|V)bFdw z!C5QrfVl!G0lzPco!$={*WwC{OX5Ta&o&WADLZg|3futu{>WkwxY+*gNp#FH9MTPDQ{1wxL4o8;7<25goUUEe}Eo4QxEANC$`zonwfxu zV0rXmo460@H}s{EhyA4RpA$HD{A-3qc#Dg4r2!@A*#|HK*V8RC16mY$35qx z$a^lJu30j`4_n6y2 zOv(zRcsYXWqu>Imq1&~f5{BM9Jsv{>JgB6Pz7pihOa4^9OV)QNBbgp`3(CCQ+s?BE zTyFfnT+4uny8@oyc^GjnGJ^ihnKD;>IJz>bl5M+$h&r+P?NgFg6wD4O9gA+KV`4ok zx=1s5p!WMckdQtJ6}8;^%?$#~;`9E^uHyXrixNm0V_+W$Wv{O^^ElHqyg+bPSJyUi z@=Xw0Tz_{~4-f%tbAm2QpBiJ**u5`$CiPVb?GmZfSKg?}4?vDU!7WYi7AM8cChYpr z%V4M30(gYuDDQJkIV|x2`I`eeUO$dXt=bE#9X)JD{cy*br*wqx)}wnemkr0NsYs3H z4)gh~b>(A)FZ8fIk!j_rCM&ZWe)Z!(zuPEF=h_E2VhE9L^ubtC|9Sp4KU@Aadw!Ho z;X+_*lhDwziv7LT0Xv0^?va=THAbdU+q{n;XU@2rfbD9SMSIV>#w&C^2bl`>R-9W! zrIH4E=3XCgeK_1RIW!{PK~$UH;L`x}MbsdnDWCOHd3)G-Q{`x7^a4!#fDr;Lqep2- zP+p8W)JIB^>F3c0LlT%ph1{|tHz$w(x>ea7lIV1!X#N*gh}vbO9LYhMghG(!MA0vu z7g#z9HlC(}7FwS!@*2~XDhN$39_5D6ya^E zqUP=$^b&{D;16&Pu&6ql-~H!socYh5Xw?@+m3Q;QnhBH*FiZEd+umZx*7yi1l#7ZX zvbZGhDSHF##g?>!Uv+*`&WjF>t?3+is+pAKoHhnodHZz4Tnu$-?rwM`zPgD7D_~fyex)oNFkt@%dfI0CU;x-9i>F~7kDChTU%QtVcL#Nc~QItR} zZje1ITKdAU;`2fykZZz#uYQ2)nhwF98uFx0t}PZKY9d7ur(lXyY%O<532K{C7bq9h z#3s6{6yGQ6$#D2DHuTinDYO*Qf7MR&F~q@6NMvEF{Ny(WN#a*i{4Em{DyNxx4!#S{x zSN7A%y)l$+OUB524H>Dy@hTZpa;%;8nepPrHrv>z zao)v%0&$O1+4T?-l7`q1Y?sDAZBu-E1rm|)iT;*^D^HE`#%eI{s zqMU1gwoWV1smC1&FC=c?CQUJa6UreoJ?GozNW8A{Rld%n2F4-Mds5tM&2E$~P{sU; zg-Gs9p^Lu}^Wrgw?G;E@k#0qAd2KlS8W_j5z9HGK;at~+G}t^(nmGN&*wUSc?3KS9 z?_5a;jxQT9)Wo8T;HD14`}UV^Nw6X0yyuEPFNKk7T6j(=vAOM!J}m($T@cpHoo4nw z!~K6rF+nm5Uo(WlT$8_UV`2y7EhOq?MECuI^^4FFHF96YG6gIPgqv5rlN#tCQG5Is z5s_9?z580F(CGNj8+%QijewC!lE8QV(r|6YyU^&U!aFgus9`2pgSQD_6LWHzyz1^w z&HC9JqV16pUT_U!{nXD{DOD<0O+&`YCWdy*q8*FggJ&B_LP-J_$!epD!~~)2^s}oz z#F^xB0e-9&Om?0?j-Sa&W#aHV>?x{|!51iJ_=*SOKzpEE+#7eKF^43}lr@@zdRJmK zd-o7z4V0@>Fi~8&H-|7$XUYWlMMqX9>AowiG1OIy+~xj1`}27s^9aaxJMsP5^?m!l OOR6erDU`i}hW-y?xL<() literal 0 HcmV?d00001 diff --git a/evbox/evbox.pro b/evbox/evbox.pro new file mode 100644 index 00000000..2126f7ef --- /dev/null +++ b/evbox/evbox.pro @@ -0,0 +1,9 @@ +include(../plugins.pri) + +QT += network serialport + +SOURCES += \ + integrationpluginevbox.cpp \ + +HEADERS += \ + integrationpluginevbox.h \ diff --git a/evbox/integrationpluginevbox.cpp b/evbox/integrationpluginevbox.cpp new file mode 100644 index 00000000..9f1188f0 --- /dev/null +++ b/evbox/integrationpluginevbox.cpp @@ -0,0 +1,348 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + +#include "integrationpluginevbox.h" +#include "plugininfo.h" +#include "plugintimer.h" + +#include +#include +#include + +#define STX 0x02 +#define ETX 0x03 + +IntegrationPluginEVBox::IntegrationPluginEVBox() +{ + +} + +IntegrationPluginEVBox::~IntegrationPluginEVBox() +{ +} + +void IntegrationPluginEVBox::discoverThings(ThingDiscoveryInfo *info) +{ + // Create the list of available serial interfaces + + foreach(QSerialPortInfo port, QSerialPortInfo::availablePorts()) { + + qCDebug(dcEVBox()) << "Found serial port:" << port.portName(); + QString description = port.portName() + " " + port.manufacturer() + " " + port.description(); + ThingDescriptor thingDescriptor(info->thingClassId(), "EVBox Elvi", description); + ParamList parameters; + foreach (Thing *existingThing, myThings()) { + if (existingThing->paramValue(evboxThingSerialPortParamTypeId).toString() == port.portName()) { + thingDescriptor.setThingId(existingThing->id()); + break; + } + } + parameters.append(Param(evboxThingSerialPortParamTypeId, port.portName())); + thingDescriptor.setParams(parameters); + info->addThingDescriptor(thingDescriptor); + } + + info->finish(Thing::ThingErrorNoError); +} + +void IntegrationPluginEVBox::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + QString interface = thing->paramValue(evboxThingSerialPortParamTypeId).toString(); + QSerialPort *serialPort = new QSerialPort(interface, info->thing()); + + serialPort->setBaudRate(QSerialPort::Baud38400); + serialPort->setDataBits(QSerialPort::Data8); + serialPort->setStopBits(QSerialPort::OneStop); + serialPort->setParity(QSerialPort::NoParity); + + connect(serialPort, &QSerialPort::readyRead, thing, [=]() { + thing->setStateValue(evboxConnectedStateTypeId, true); + QByteArray data = serialPort->readAll(); +// qCDebug(dcEVBox()) << "Data received from serial port:" << data; + m_inputBuffers[thing].append(data); + processInputBuffer(thing); + }); + + connect(serialPort, static_cast(&QSerialPort::error), thing, [=](){ + qCWarning(dcEVBox()) << "Serial Port error" << serialPort->error() << serialPort->errorString(); + if (serialPort->error() != QSerialPort::NoError) { + if (serialPort->isOpen()) { + serialPort->close(); + } + thing->setStateValue(evboxConnectedStateTypeId, false); + QTimer::singleShot(1000, this, [=](){ + serialPort->open(QSerialPort::ReadWrite); + }); + } + }); + + if (!serialPort->open(QSerialPort::ReadWrite)) { + qCWarning(dcEVBox()) << "Unable to open serial port"; + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to open the RS485 port. Please make sure the RS485 adapter is connected properly.")); + return; + } + + m_serialPorts.insert(thing, serialPort); + + m_pendingSetups.insert(thing, info); + connect(info, &ThingSetupInfo::finished, this, [=](){ + m_pendingSetups.remove(thing); + }); + QTimer::singleShot(2000, info, [=](){ + qCDebug(dcEVBox()) << "Timeout during setup"; + delete m_serialPorts.take(info->thing()); + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The EVBox is not responding.")); + }); + + + sendCommand(thing, Command69, 1); +} + +void IntegrationPluginEVBox::thingRemoved(Thing *thing) +{ + m_timers.remove(thing); + delete m_serialPorts.take(thing); +} + +void IntegrationPluginEVBox::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + + if (info->action().actionTypeId() == evboxPowerActionTypeId) { + bool power = info->action().paramValue(evboxPowerActionPowerParamTypeId).toBool(); + sendCommand(info->thing(), Command69, power ? info->thing()->stateValue(evboxMaxChargingCurrentStateTypeId).toUInt() : 0); + } else if (info->action().actionTypeId() == evboxMaxChargingCurrentActionTypeId) { + int maxChargingCurrent = info->action().paramValue(evboxMaxChargingCurrentActionMaxChargingCurrentParamTypeId).toInt(); + sendCommand(info->thing(), Command69, maxChargingCurrent); + } + + m_pendingActions[thing].append(info); + connect(info, &ThingActionInfo::finished, this, [=](){ + m_pendingActions[thing].removeAll(info); + }); + +} + +bool IntegrationPluginEVBox::sendCommand(Thing *thing, Command command, quint16 maxChargingCurrent) +{ + QByteArray commandData; + + commandData += "80"; // Dst addr + commandData += "A0"; // Sender address + commandData += QString::number(command); + commandData += QString("%1").arg(maxChargingCurrent * 10, 4, 10, QChar('0')); + commandData += QString("%1").arg(maxChargingCurrent * 10, 4, 10, QChar('0')); + commandData += QString("%1").arg(maxChargingCurrent * 10, 4, 10, QChar('0')); + commandData += "003C"; // Timeout (60 sec) + // If we fail to refresh the wallbox after the timeout, it shall turn off, which is what we'll use as default + // when we don't know what its set to (as we can't read it). + // Hence we do *not* cache the power and maxChargingCurrent states for this one + commandData += QString("%1").arg(0, 4, 10, QChar('0')); + commandData += QString("%1").arg(0, 4, 10, QChar('0')); + commandData += QString("%1").arg(0, 4, 10, QChar('0')); + + commandData += createChecksum(commandData); + + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + stream << static_cast(STX); + stream.writeRawData(commandData.data(), commandData.length()); + stream << static_cast(ETX); + + qCDebug(dcEVBox()) << "Writing data:" << data << "->" << data.toHex(); + QSerialPort *serialPort = m_serialPorts.value(thing); + qint64 count = serialPort->write(data); + if (count == data.length()) { + m_waitingForResponses[thing] = true; + } + return count == data.length(); +} + +QByteArray IntegrationPluginEVBox::createChecksum(const QByteArray &data) const +{ + QDataStream checksumStream(data); + quint8 sum = 0; + quint8 xOr = 0; + while (!checksumStream.atEnd()) { + quint8 byte; + checksumStream >> byte; + sum += byte; + xOr ^= byte; + } + return QString("%1%2").arg(sum,2,16, QChar('0')).arg(xOr,2,16, QChar('0')).toUpper().toLocal8Bit(); +} + +void IntegrationPluginEVBox::processInputBuffer(Thing *thing) +{ + QByteArray packet; + QDataStream inputStream(m_inputBuffers.value(thing)); + QDataStream outputStream(&packet, QIODevice::WriteOnly); + bool startFound = false, endFound = false; + + while (!inputStream.atEnd()) { + quint8 byte; + inputStream >> byte; + if (!startFound) { + if (byte == STX) { + startFound = true; + continue; + } else { + qCWarning(dcEVBox()) << "Discarding byte not matching start of frame 0x" + QString::number(byte, 16); + continue; + } + } + + if (byte == ETX) { + endFound = true; + break; + } + + outputStream << byte; + } + + if (startFound && endFound) { + m_inputBuffers[thing].remove(0, packet.length() + 2); + } else { +// qCDebug(dcEVBox()) << "Data is incomplete... Waiting for more..."; + return; + } + + if (packet.length() < 2) { // In practice it'll be longer, but let's make sure we won't crash checking the checksum on erraneous data + qCWarning(dcEVBox()) << "Packet is too short. Discarding packet..."; + return; + } + + qCDebug(dcEVBox()) << "Packet received:" << packet; + + QByteArray checksum = createChecksum(packet.left(packet.length() - 4)); + if (checksum != packet.right(4)) { + qCWarning(dcEVBox()) << "Checksum mismatch for incoming packet:" << packet << "Given checksum:" << packet.right(4) << "Expected:" << checksum; + return; + } + + // We received something valid... Assuming the last command we've sent is OK. + // There's no way to properly match a response to a command, so... + if (m_pendingSetups.contains(thing)) { + qCDebug(dcEVBox()) << "Finishing setup"; + + // Can't use a pluginTimer because it may collide with data on the wire, so we're + // manually re-starting the timer whenever we receive something. + QTimer *timer = new QTimer(thing); + m_timers.insert(thing, timer); + timer->setInterval(1000); + + connect(timer, &QTimer::timeout, thing, [=](){ + thing->setStateValue(evboxConnectedStateTypeId, !m_waitingForResponses[thing]); + + if (thing->stateValue(evboxPowerStateTypeId).toBool()) { + sendCommand(thing, Command69, thing->stateValue(evboxMaxChargingCurrentStateTypeId).toDouble()); + } else { + sendCommand(thing, Command69, 0); + } + }); + + m_pendingSetups.take(thing)->finish(Thing::ThingErrorNoError); + } + if (!m_pendingActions.value(thing).isEmpty()) { + ThingActionInfo *info = m_pendingActions.value(thing).first(); + if (info->action().actionTypeId() == evboxPowerActionTypeId) { + thing->setStateValue(evboxPowerStateTypeId, info->action().paramValue(evboxPowerActionPowerParamTypeId)); + } else if (info->action().actionTypeId() == evboxMaxChargingCurrentActionTypeId) { + thing->setStateValue(evboxMaxChargingCurrentStateTypeId, info->action().paramValue(evboxMaxChargingCurrentActionMaxChargingCurrentParamTypeId)); + } + info->finish(Thing::ThingErrorNoError); + } + + processDataPacket(thing, packet); +} + +void IntegrationPluginEVBox::processDataPacket(Thing *thing, const QByteArray &packet) +{ + + // The data is a mess of hex and dec values... So do not wonder about the weird from/to hex mess in here... + QDataStream stream(QByteArray::fromHex(packet)); + + quint8 from, to, commandId, wallboxCount; + quint16 minPollInterval, maxChargingCurrent; + stream >> from >> to >> commandId >> minPollInterval >> maxChargingCurrent >> wallboxCount; + + commandId = QString::number(commandId, 16).toInt(); + + qCDebug(dcEVBox()) << QString("From: %1, To: %2, CMD: %3, MinPollInterval: %4, maxChargingCurrent: %5, Wallbox data count: %6") + .arg(from).arg(to).arg(commandId).arg(minPollInterval).arg(maxChargingCurrent).arg(wallboxCount); + + if (commandId != Command69) { + qCWarning(dcEVBox()) << "Only command 69 is implemented! Adjust response parsing if sending other commands."; + return; + } + + m_waitingForResponses[thing] = false; + + // Command 69 would give a list of wallboxes (they can be chained apparently) but we only support a single one for now +// for (int i = 0; i < wallboxCount; i++) { + + if (wallboxCount > 0) { + quint16 minChargingCurrent, chargingCurrentL1, chargingCurrentL2, chargingCurrentL3, cosinePhiL1, cosinePhiL2, cosinePhiL3, totalEnergyConsumed; + stream >> minChargingCurrent >> chargingCurrentL1 >> chargingCurrentL2 >> chargingCurrentL3 >> cosinePhiL1 >> cosinePhiL2 >> cosinePhiL3 >> totalEnergyConsumed; + + qCDebug(dcEVBox()) << QString("Min current: %1, actual current L1: %2, L2: %3, L3: %4, Total energy: %5") + .arg(minChargingCurrent).arg(chargingCurrentL1).arg(chargingCurrentL2).arg(chargingCurrentL3).arg(totalEnergyConsumed); + + thing->setStateMinMaxValues(evboxMaxChargingCurrentStateTypeId, minChargingCurrent / 10, maxChargingCurrent / 10); + + double currentPower = (chargingCurrentL1 + chargingCurrentL2 + chargingCurrentL3) * 23; + thing->setStateValue(evboxCurrentPowerStateTypeId, currentPower); + + thing->setStateValue(evboxTotalEnergyConsumedStateTypeId, totalEnergyConsumed / 1000.0); + + thing->setStateValue(evboxChargingStateTypeId, currentPower > 0); + + int phaseCount = 0; + if (chargingCurrentL1 > 0) { + phaseCount++; + } + if (chargingCurrentL2 > 0) { + phaseCount++; + } + if (chargingCurrentL3 > 0) { + phaseCount++; + } + // If all phases are on 0, we aren't charging and don't know how may phases are used... + // so only updating the count if we actually do know that at least one is charging. + if (phaseCount > 0) { + thing->setStateValue(evboxPhaseCountStateTypeId, phaseCount); + } + } + + m_timers.value(thing)->start(); +} + diff --git a/evbox/integrationpluginevbox.h b/evbox/integrationpluginevbox.h new file mode 100644 index 00000000..fe663d55 --- /dev/null +++ b/evbox/integrationpluginevbox.h @@ -0,0 +1,83 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2022, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINEVBOX_H +#define INTEGRATIONPLUGINEVBOX_H + +#include "integrations/integrationplugin.h" + +#include "extern-plugininfo.h" + +#include + +class QSerialPort; + +class IntegrationPluginEVBox: public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginevbox.json") + Q_INTERFACES(IntegrationPlugin) + +public: + enum Command { + Command68 = 68, + Command69 = 69 + }; + Q_ENUM(Command) + + explicit IntegrationPluginEVBox(); + ~IntegrationPluginEVBox(); + + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void thingRemoved(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + +private: + bool sendCommand(Thing *thing, Command command, quint16 maxChargingCurrent); + + QByteArray createChecksum(const QByteArray &data) const; + + void processInputBuffer(Thing *thing); + void processDataPacket(Thing *thing, const QByteArray &packet); + +private: + QHash m_serialPorts; + QHash m_pendingSetups; + QHash> m_pendingActions; + + QHash m_inputBuffers; + + QHash m_timers; + QHash m_waitingForResponses; +}; + +#endif // INTEGRATIONPLUGINEVBOX_H diff --git a/evbox/integrationpluginevbox.json b/evbox/integrationpluginevbox.json new file mode 100644 index 00000000..ec9a1959 --- /dev/null +++ b/evbox/integrationpluginevbox.json @@ -0,0 +1,97 @@ +{ + "name": "EVBox", + "displayName": "EVBox", + "id": "3362ac5c-5e2f-43c0-b3fc-70a98773e119", + "vendors": [ + { + "name": "evbox", + "displayName": "EVBox", + "id": "435d8843-887a-4642-b2f5-cd27d18bdb95", + "thingClasses": [ + { + "id": "d73a14e3-10af-47bc-9bc7-a5ff6e52f72c", + "name": "evbox", + "displayName": "Elvi", + "createMethods": ["discovery"], + "setupMethod": "justadd", + "interfaces": [ "evcharger", "connectable" ], + "paramTypes": [ + { + "id": "bce7c412-c19a-4e60-a11f-fe8308408abf", + "name":"serialPort", + "displayName": "Serial port", + "type": "QString" + } + ], + "stateTypes": [ + { + "id": "5ef06038-9fa9-4d5d-8d9b-0375b8aa343a", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "e3ed9334-68bf-47eb-bd9a-d9a800529bce", + "name": "power", + "displayName": "Charging enabled", + "displayNameAction": "Enable/disable charging", + "type": "bool", + "defaultValue": false, + "writable": true, + "cached": false + }, + { + "id": "cc9ae86d-fc86-473f-ae90-d9eb20d7a011", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameAction": "Set maximum charging current", + "type": "uint", + "writable": true, + "unit": "Ampere", + "minValue": "6", + "maxValue": "22", + "defaultValue": 6, + "cached": false + }, + { + "id": "8d3c80b7-f1f1-48de-8b7a-f99b9bc688b7", + "name": "currentPower", + "displayName": "Current power consumption", + "type": "double", + "unit": "Watt", + "defaultValue": 0, + "cached": false + }, + { + "id": "9fd15d14-c228-4af6-85af-4cb171d6f9f0", + "name": "totalEnergyConsumed", + "displayName": "Total consumed energy", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0 + }, + { + "id": "6439fdba-dc03-454f-bc33-0f6e2619d2ab", + "name": "charging", + "displayName": "Charging", + "type": "bool", + "defaultValue": false + }, + { + "id": "1120abe3-1878-4301-a701-014b24fd1e41", + "name": "phaseCount", + "displayName": "Used phases", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 1 + } + ] + } + ] + } + ] +} diff --git a/evbox/meta.json b/evbox/meta.json new file mode 100644 index 00000000..a0d966b3 --- /dev/null +++ b/evbox/meta.json @@ -0,0 +1,13 @@ +{ + "title": "EVBox", + "tagline": "Integrates EVBox wallboxes with nymea.", + "icon": "evbox-logo-blue.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "serial-port" + ], + "categories": [ + "energy" + ] +} diff --git a/evbox/translations/3362ac5c-5e2f-43c0-b3fc-70a98773e119-en_US.ts b/evbox/translations/3362ac5c-5e2f-43c0-b3fc-70a98773e119-en_US.ts new file mode 100644 index 00000000..0d41216c --- /dev/null +++ b/evbox/translations/3362ac5c-5e2f-43c0-b3fc-70a98773e119-en_US.ts @@ -0,0 +1,101 @@ + + + + + EVBox + + + Charging + The name of the StateType ({6439fdba-dc03-454f-bc33-0f6e2619d2ab}) of ThingClass evbox + + + + + + Charging enabled + The name of the ParamType (ThingClass: evbox, ActionType: power, ID: {e3ed9334-68bf-47eb-bd9a-d9a800529bce}) +---------- +The name of the StateType ({e3ed9334-68bf-47eb-bd9a-d9a800529bce}) of ThingClass evbox + + + + + Connected + The name of the StateType ({5ef06038-9fa9-4d5d-8d9b-0375b8aa343a}) of ThingClass evbox + + + + + Current power consumption + The name of the StateType ({8d3c80b7-f1f1-48de-8b7a-f99b9bc688b7}) of ThingClass evbox + + + + + + EVBox + The name of the vendor ({435d8843-887a-4642-b2f5-cd27d18bdb95}) +---------- +The name of the plugin EVBox ({3362ac5c-5e2f-43c0-b3fc-70a98773e119}) + + + + + Elvi + The name of the ThingClass ({d73a14e3-10af-47bc-9bc7-a5ff6e52f72c}) + + + + + Enable/disable charging + The name of the ActionType ({e3ed9334-68bf-47eb-bd9a-d9a800529bce}) of ThingClass evbox + + + + + + Maximum charging current + The name of the ParamType (ThingClass: evbox, ActionType: maxChargingCurrent, ID: {cc9ae86d-fc86-473f-ae90-d9eb20d7a011}) +---------- +The name of the StateType ({cc9ae86d-fc86-473f-ae90-d9eb20d7a011}) of ThingClass evbox + + + + + Serial port + The name of the ParamType (ThingClass: evbox, Type: thing, ID: {bce7c412-c19a-4e60-a11f-fe8308408abf}) + + + + + Set maximum charging current + The name of the ActionType ({cc9ae86d-fc86-473f-ae90-d9eb20d7a011}) of ThingClass evbox + + + + + Total consumed energy + The name of the StateType ({9fd15d14-c228-4af6-85af-4cb171d6f9f0}) of ThingClass evbox + + + + + Used phases + The name of the StateType ({1120abe3-1878-4301-a701-014b24fd1e41}) of ThingClass evbox + + + + + IntegrationPluginEVBox + + + Unable to open the RS485 port. Please make sure the RS485 adapter is connected properly. + + + + + The EVBox is not responding. + + + + diff --git a/nymea-plugins.pro b/nymea-plugins.pro index f6d3babf..7c714647 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -22,6 +22,7 @@ PLUGIN_DIRS = \ elgato \ eq-3 \ espuino \ + evbox \ fastcom \ flowercare \ fronius \