mirror of
https://github.com/nymea/nymea-plugins.git
synced 2026-07-25 01:46:59 +02:00
New Plugin: Solarman
This commit is contained in:
parent
860fbac0e8
commit
c771aee038
@ -53,18 +53,19 @@ PLUGIN_DIRS = \
|
||||
powerfox \
|
||||
pushbullet \
|
||||
pushnotifications \
|
||||
shelly \
|
||||
solarlog \
|
||||
systemmonitor \
|
||||
reversessh \
|
||||
senic \
|
||||
serialportcommander \
|
||||
sgready \
|
||||
shelly \
|
||||
simpleheatpump \
|
||||
sma \
|
||||
solarlog \
|
||||
solarman \
|
||||
somfytahoma \
|
||||
sonos \
|
||||
sunposition \
|
||||
systemmonitor \
|
||||
tado \
|
||||
tasmota \
|
||||
tcpcommander \
|
||||
|
||||
21
solarman/README.md
Normal file
21
solarman/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Solarman
|
||||
|
||||
This integration plugin allows to add Solarman compatible inverters to nymea.
|
||||
|
||||
Every inverter which is compatible with the Solarman app should be possible to use with this
|
||||
plugin, provided the Modbus registers for the inverter are defined in the registermappings.json.
|
||||
|
||||
## Current supported devices
|
||||
|
||||
* Bosswerk MI-300
|
||||
* Bosswerk MI-600
|
||||
|
||||
## Requirements
|
||||
|
||||
The solar inverter needs to be connected to the same network as nymea. Once powered on, the
|
||||
inverter will open a wireless hotspot named AP_XXXXXXXXXX where XXXXXXXXXX is the serial number
|
||||
written on the casing. The default password for this WiFi is 12345678. After connecting,
|
||||
open [http://10.10.100.254](http://10.10.100.254) with the browser and use the web interface
|
||||
on the inverter to connect it to a WiFi with nymea in it.
|
||||
|
||||
Once done so, set up the inverter in nymea as any other thing.
|
||||
213
solarman/integrationpluginsolarman.cpp
Normal file
213
solarman/integrationpluginsolarman.cpp
Normal file
@ -0,0 +1,213 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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 "integrationpluginsolarman.h"
|
||||
#include "plugininfo.h"
|
||||
|
||||
#include <plugintimer.h>
|
||||
#include <network/networkdevicediscovery.h>
|
||||
#include <network/networkaccessmanager.h>
|
||||
#include <network/networkdevicediscoveryreply.h>
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QAuthenticator>
|
||||
#include <QUrlQuery>
|
||||
#include <QJsonDocument>
|
||||
#include <QMetaEnum>
|
||||
#include <QUdpSocket>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
|
||||
#include "solarmandiscovery.h"
|
||||
#include "solarmanmodbus.h"
|
||||
#include "solarmanmodbusreply.h"
|
||||
|
||||
IntegrationPluginSolarman::IntegrationPluginSolarman()
|
||||
{
|
||||
QFile registerMappings(":/registermappings.json");
|
||||
registerMappings.open(QFile::ReadOnly);
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(registerMappings.readAll());
|
||||
m_registerMappings = jsonDoc.toVariant().toMap();
|
||||
}
|
||||
|
||||
IntegrationPluginSolarman::~IntegrationPluginSolarman()
|
||||
{
|
||||
}
|
||||
|
||||
void IntegrationPluginSolarman::discoverThings(ThingDiscoveryInfo *info)
|
||||
{
|
||||
SolarmanDiscovery *discovery = new SolarmanDiscovery(info);
|
||||
connect(discovery, &SolarmanDiscovery::discoveryResults, info, [this, info](const QList<SolarmanDiscovery::DiscoveryResult> &results) {
|
||||
foreach (const SolarmanDiscovery::DiscoveryResult &result, results) {
|
||||
ThingDescriptor descriptor(deyeStringThingClassId, "Deye String Inverter", "Serial: " + result.serial + ", IP: " + result.ip.toString());
|
||||
descriptor.setParams({{deyeStringThingSerialParamTypeId, result.serial}});
|
||||
|
||||
if (myThings().findByParams(descriptor.params())) {
|
||||
descriptor.setThingId(myThings().findByParams(descriptor.params())->id());
|
||||
}
|
||||
info->addThingDescriptor(descriptor);
|
||||
}
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
discovery->discover();
|
||||
}
|
||||
|
||||
void IntegrationPluginSolarman::setupThing(ThingSetupInfo *info)
|
||||
{
|
||||
Thing *thing = info->thing();
|
||||
|
||||
SolarmanDiscovery *monitor = m_monitors.value(thing);
|
||||
if (!monitor) {
|
||||
monitor = new SolarmanDiscovery(thing);
|
||||
m_monitors.insert(thing, monitor);
|
||||
}
|
||||
|
||||
if (!monitor->monitor(thing->paramValue(deyeStringThingSerialParamTypeId).toString())) {
|
||||
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unalbe to open network connection."));
|
||||
m_monitors.take(thing)->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
SolarmanModbus *modbus = new SolarmanModbus(thing);
|
||||
m_connections.insert(thing, modbus);
|
||||
|
||||
// For testing
|
||||
// modbus->connectToHost(monitor->monitorResult().ip, 8899, monitor->monitorResult().serial);
|
||||
// pollDevice(thing);
|
||||
|
||||
connect(monitor, &SolarmanDiscovery::monitorStateChanged, thing, [modbus, monitor](const SolarmanDiscovery::DiscoveryResult &result) {
|
||||
if (result.online && !modbus->isConnected()) {
|
||||
modbus->connectToHost(monitor->monitorResult().ip, 8899, monitor->monitorResult().serial);
|
||||
}
|
||||
});
|
||||
|
||||
connect(modbus, &SolarmanModbus::connectedChanged, thing, [this, thing, monitor, modbus](bool connected){
|
||||
thing->setStateValue(deyeStringConnectedStateTypeId, connected);
|
||||
if (connected) {
|
||||
pollDevice(thing);
|
||||
m_timers.value(thing)->reset();
|
||||
m_timers.value(thing)->start();
|
||||
} else {
|
||||
m_timers.value(thing)->stop();
|
||||
if (monitor->monitorResult().online) {
|
||||
qCDebug(dcSolarman()) << "Reconnecting to solarman inverter";
|
||||
modbus->connectToHost(monitor->monitorResult().ip, 8899, monitor->monitorResult().serial);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
PluginTimer *timer = m_timers.take(thing);
|
||||
if (!timer) {
|
||||
// the inverter updates its internal values once per minute...
|
||||
timer = hardwareManager()->pluginTimerManager()->registerTimer(21);
|
||||
m_timers.insert(thing, timer);
|
||||
|
||||
connect(timer, &PluginTimer::timeout, thing, [this, thing](){
|
||||
pollDevice(thing);
|
||||
});
|
||||
}
|
||||
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
}
|
||||
|
||||
void IntegrationPluginSolarman::thingRemoved(Thing *thing)
|
||||
{
|
||||
m_monitors.remove(thing);
|
||||
m_connections.remove(thing);
|
||||
hardwareManager()->pluginTimerManager()->unregisterTimer(m_timers.take(thing));
|
||||
}
|
||||
|
||||
void IntegrationPluginSolarman::pollDevice(Thing *thing)
|
||||
{
|
||||
SolarmanModbus *modbus = m_connections.value(thing);
|
||||
if (!modbus->isConnected()) {
|
||||
qCDebug(dcSolarman()) << "Inverter offline. Not polling.";
|
||||
return;
|
||||
}
|
||||
|
||||
QVariantMap mapping = m_registerMappings.value(thing->thingClass().name()).toMap();
|
||||
quint8 slaveId = mapping.value("slaveId").toUInt();
|
||||
quint16 startRegister = mapping.value("startRegister").toUInt();
|
||||
quint16 endRegister = mapping.value("endRegister").toUInt();
|
||||
QMetaEnum fcEnum = QMetaEnum::fromType<SolarmanModbus::FunctionCode>();
|
||||
SolarmanModbus::FunctionCode functionCode = static_cast<SolarmanModbus::FunctionCode>(fcEnum.keyToValue(mapping.value("functionCode").toByteArray()));
|
||||
QVariantList registers = mapping.value("registers").toList();
|
||||
|
||||
|
||||
SolarmanModbusReply *reply = modbus->readRegisters(slaveId, startRegister, endRegister, functionCode);
|
||||
qCDebug(dcSolarman()) << "Polling inverter" << thing->name() << "slaveID:" << slaveId << "Start:" << startRegister << "End:" << endRegister << "Request ID" << reply->requestId();
|
||||
connect(reply, &SolarmanModbusReply::finished, thing, [modbus, thing, reply, registers](bool success){
|
||||
if (!success) {
|
||||
qCWarning(dcSolarman()) << "Polling failed..." << reply->requestId();
|
||||
modbus->disconnectFromHost();
|
||||
return;
|
||||
}
|
||||
qCDebug(dcSolarman()) << "modbus reply for" << thing->name() << reply->requestId();
|
||||
|
||||
foreach (const QVariant ®isterVariant, registers) {
|
||||
QVariantMap definition = registerVariant.toMap();
|
||||
quint16 reg = definition.value("index").toUInt();
|
||||
quint8 length = definition.value("length").toUInt();
|
||||
QString type = definition.value("type").toString();
|
||||
QString stateName = definition.value("state").toString();
|
||||
QString registerName = definition.value("name").toString();
|
||||
int offset = definition.value("offset", 0).toInt();
|
||||
double scale = definition.value("scale", 1).toDouble();
|
||||
|
||||
|
||||
QVariant value;
|
||||
if (type == "uint16") {
|
||||
double val = reply->readRegister16(reg);
|
||||
val += offset;
|
||||
val *= scale;
|
||||
value = val;
|
||||
|
||||
} else if (type == "uint32") {
|
||||
double val = reply->readRegister32(reg);
|
||||
val += offset;
|
||||
val *= scale;
|
||||
value = val;
|
||||
} else if (type == "string") {
|
||||
value = reply->readRegisterString(reg, length);
|
||||
}
|
||||
qCDebug(dcSolarman()) << "Register" << reg << registerName << ":" << value;
|
||||
if (!stateName.isEmpty()) {
|
||||
thing->setStateValue(stateName, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// qCDebug(dcSolarman()) << "Today's production:" << reply->readRegister16(0x003C);
|
||||
// qCDebug(dcSolarman()) << "Inverter status:" << reply->readRegister16(0x003B);
|
||||
// qCDebug(dcSolarman()) << "Total production:" << reply->readRegister32(0x003F);
|
||||
});
|
||||
|
||||
}
|
||||
73
solarman/integrationpluginsolarman.h
Normal file
73
solarman/integrationpluginsolarman.h
Normal file
@ -0,0 +1,73 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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 INTEGRATIONPLUGINSOLARMAN_H
|
||||
#define INTEGRATIONPLUGINSOLARMAN_H
|
||||
|
||||
#include "integrations/integrationplugin.h"
|
||||
#include "extern-plugininfo.h"
|
||||
|
||||
class PluginTimer;
|
||||
class SolarmanDiscovery;
|
||||
class SolarmanModbus;
|
||||
|
||||
class IntegrationPluginSolarman: public IntegrationPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginsolarman.json")
|
||||
Q_INTERFACES(IntegrationPlugin)
|
||||
|
||||
public:
|
||||
enum Method {
|
||||
GET,
|
||||
SET
|
||||
};
|
||||
Q_ENUM(Method)
|
||||
|
||||
explicit IntegrationPluginSolarman();
|
||||
~IntegrationPluginSolarman();
|
||||
|
||||
void discoverThings(ThingDiscoveryInfo *info) override;
|
||||
void setupThing(ThingSetupInfo *info) override;
|
||||
void thingRemoved(Thing *thing) override;
|
||||
|
||||
private slots:
|
||||
void pollDevice(Thing *thing);
|
||||
|
||||
private:
|
||||
QHash<Thing*, SolarmanDiscovery*> m_monitors;
|
||||
QHash<Thing*, SolarmanModbus*> m_connections;
|
||||
QHash<Thing*, PluginTimer*> m_timers;
|
||||
|
||||
QVariantMap m_registerMappings;
|
||||
};
|
||||
|
||||
#endif // INTEGRATIONPLUGINSOLARMAN_H
|
||||
82
solarman/integrationpluginsolarman.json
Normal file
82
solarman/integrationpluginsolarman.json
Normal file
@ -0,0 +1,82 @@
|
||||
{
|
||||
"name": "solarman",
|
||||
"displayName": "Solarman",
|
||||
"id": "9a37f9db-4b82-4bbc-b1ab-7eed5a1f4b70",
|
||||
"vendors": [
|
||||
{
|
||||
"name": "solarman",
|
||||
"displayName": "Solarman",
|
||||
"id": "d6bc0ecd-8cbe-4d5d-a606-0712c5f10978",
|
||||
"thingClasses": [
|
||||
{
|
||||
"id": "9712b0f0-45a3-4c27-a675-4ef6a0f1dc1e",
|
||||
"name": "deyeString",
|
||||
"displayName": "Deye String Inverter",
|
||||
"createMethods": ["discovery"],
|
||||
"interfaces": [ "solarinverter", "wirelessconnectable" ],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "e23e8186-e3df-4dc1-87cf-59627e1d2a3d",
|
||||
"name":"serial",
|
||||
"displayName": "Serial number",
|
||||
"type": "QString"
|
||||
}
|
||||
],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "7b110485-7b12-425b-b4c2-6a0a76db69cf",
|
||||
"name": "connected",
|
||||
"displayName": "Connected",
|
||||
"displayNameEvent": "Connected changed",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "1303b3fa-072f-468e-949c-2e1474ce8f1e",
|
||||
"name": "signalStrength",
|
||||
"displayName": "Signal strength",
|
||||
"displayNameEvent": "Signal strength changed",
|
||||
"type": "uint",
|
||||
"unit": "Percentage",
|
||||
"minValue": 0,
|
||||
"maxValue": 100,
|
||||
"defaultValue": 0,
|
||||
"filter": "adaptive",
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "49e29f3b-d04b-4972-94e1-d5485405f716",
|
||||
"name": "currentPower",
|
||||
"displayName": "Current power consumption",
|
||||
"displayNameEvent": "Current power consumption changed",
|
||||
"type": "double",
|
||||
"unit": "Watt",
|
||||
"defaultValue": 0,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "0118a424-6f93-4f37-a9da-17617b508a45",
|
||||
"name": "totalEnergyProduced",
|
||||
"displayName": "Total produced energy",
|
||||
"displayNameEvent": "Total produced energy changed",
|
||||
"type": "double",
|
||||
"unit": "KiloWattHour",
|
||||
"defaultValue": 0
|
||||
},
|
||||
{
|
||||
"id": "bc8ed7f6-6139-43e4-8d49-32ec5020d493",
|
||||
"name": "temperature",
|
||||
"displayName": "Inverter temperature",
|
||||
"displayNameEvent": "Inverter temperature changed",
|
||||
"type": "double",
|
||||
"unit": "DegreeCelsius",
|
||||
"defaultValue": 0
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
13
solarman/meta.json
Normal file
13
solarman/meta.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "Solarman",
|
||||
"tagline": "Integrates Solarman compatible solar (Deye, Bosswerk...) inverters with nymea.",
|
||||
"icon": "solarman.png",
|
||||
"stability": "consumer",
|
||||
"offline": true,
|
||||
"technologies": [
|
||||
"network"
|
||||
],
|
||||
"categories": [
|
||||
"energy"
|
||||
]
|
||||
}
|
||||
97
solarman/registermappings.json
Normal file
97
solarman/registermappings.json
Normal file
@ -0,0 +1,97 @@
|
||||
{
|
||||
"deyeString": {
|
||||
"slaveId": 1,
|
||||
"startRegister": 1,
|
||||
"endRegister": 116,
|
||||
"functionCode": "ReadHoldingRegisters",
|
||||
"registers": [
|
||||
{
|
||||
"index": 3,
|
||||
"type": "string",
|
||||
"length": 5,
|
||||
"name": "Inverter serial"
|
||||
},
|
||||
{
|
||||
"index": 16,
|
||||
"type": "uint16",
|
||||
"name": "Rated power",
|
||||
"scale": 0.1
|
||||
},
|
||||
{
|
||||
"index": 59,
|
||||
"type": "uint16",
|
||||
"name": "Inverter status"
|
||||
},
|
||||
{
|
||||
"index": 60,
|
||||
"type": "uint16",
|
||||
"name": "Daily production (Active)",
|
||||
"scale": 0.1
|
||||
},
|
||||
{
|
||||
"index": 63,
|
||||
"type": "uint32",
|
||||
"name": "Total Production (Active)",
|
||||
"state": "totalEnergyProduced",
|
||||
"scale": 0.1
|
||||
},
|
||||
{
|
||||
"index": 73,
|
||||
"type": "uint16",
|
||||
"name": "AC Voltage 1",
|
||||
"scale": 0.1
|
||||
},
|
||||
{
|
||||
"index": 74,
|
||||
"type": "uint16",
|
||||
"name": "AC Voltage 2",
|
||||
"scale": 0.1
|
||||
},
|
||||
{
|
||||
"index": 75,
|
||||
"type": "uint16",
|
||||
"name": "AC Voltage 3",
|
||||
"scale": 0.1
|
||||
},
|
||||
{
|
||||
"index": 76,
|
||||
"type": "uint16",
|
||||
"name": "AC Current 1",
|
||||
"scale": 0.1
|
||||
},
|
||||
{
|
||||
"index": 77,
|
||||
"type": "uint16",
|
||||
"name": "AC Current 2",
|
||||
"scale": 0.1
|
||||
},
|
||||
{
|
||||
"index": 78,
|
||||
"type": "uint16",
|
||||
"name": "AC Current 3",
|
||||
"scale": 0.1
|
||||
},
|
||||
{
|
||||
"index": 79,
|
||||
"type": "uint16",
|
||||
"name": "AC Output Frequency",
|
||||
"scale": 0.01
|
||||
},
|
||||
{
|
||||
"index": 86,
|
||||
"name": "Total AC Output Power (Active)",
|
||||
"type": "uint32",
|
||||
"state": "currentPower",
|
||||
"scale": -0.1
|
||||
},
|
||||
{
|
||||
"index": 90,
|
||||
"name": "Temerpature",
|
||||
"type": "uint16",
|
||||
"state": "temperature",
|
||||
"offset": -1000,
|
||||
"scale": 0.01
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
5
solarman/registermappings.qrc
Normal file
5
solarman/registermappings.qrc
Normal file
@ -0,0 +1,5 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>registermappings.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
BIN
solarman/solarman.png
Normal file
BIN
solarman/solarman.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
21
solarman/solarman.pro
Normal file
21
solarman/solarman.pro
Normal file
@ -0,0 +1,21 @@
|
||||
include(../plugins.pri)
|
||||
|
||||
QT += network
|
||||
|
||||
SOURCES += \
|
||||
integrationpluginsolarman.cpp \
|
||||
solarmandiscovery.cpp \
|
||||
solarmanmodbus.cpp \
|
||||
solarmanmodbusreply.cpp
|
||||
|
||||
HEADERS += \
|
||||
integrationpluginsolarman.h \
|
||||
solarmandiscovery.h \
|
||||
solarmanmodbus.h \
|
||||
solarmanmodbusreply.h
|
||||
|
||||
DISTFILES += \
|
||||
registermappings.json
|
||||
|
||||
RESOURCES += \
|
||||
registermappings.qrc
|
||||
152
solarman/solarmandiscovery.cpp
Normal file
152
solarman/solarmandiscovery.cpp
Normal file
@ -0,0 +1,152 @@
|
||||
#include "solarmandiscovery.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QUdpSocket>
|
||||
|
||||
#include <QLoggingCategory>
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcSolarman)
|
||||
|
||||
SolarmanDiscovery::SolarmanDiscovery(QObject *parent)
|
||||
: QObject{parent}
|
||||
{
|
||||
}
|
||||
|
||||
bool SolarmanDiscovery::discover()
|
||||
{
|
||||
if (!initUdp()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_discoveryTimer) {
|
||||
qCDebug(dcSolarman()) << "Already discovering...";
|
||||
return true;
|
||||
}
|
||||
|
||||
m_discoveryTimer = new QTimer(this);
|
||||
m_discoveryTimer->start(1000);
|
||||
|
||||
connect(m_discoveryTimer, &QTimer::timeout, this, [=](){
|
||||
int counter = m_discoveryTimer->property("counter").toInt();
|
||||
if (counter < 5) {
|
||||
m_discoveryTimer->setProperty("counter", counter+1);
|
||||
sendDiscoveryMessage();
|
||||
} else {
|
||||
emit discoveryResults(m_discoveryResults);
|
||||
m_discoveryResults.clear();
|
||||
delete m_discoveryTimer;
|
||||
m_discoveryTimer = nullptr;
|
||||
}
|
||||
});
|
||||
|
||||
bool status = sendDiscoveryMessage();
|
||||
if (!status) {
|
||||
delete m_discoveryTimer;
|
||||
m_discoveryTimer = nullptr;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
bool SolarmanDiscovery::monitor(const QString &serial)
|
||||
{
|
||||
if (!initUdp()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_monitoringResult.serial = serial;
|
||||
|
||||
if (!m_monitorTimer) {
|
||||
m_monitorTimer = new QTimer(this);
|
||||
m_monitorTimer->start(15000);
|
||||
connect(m_monitorTimer, &QTimer::timeout, this, [=](){
|
||||
sendDiscoveryMessage();
|
||||
|
||||
if (m_monitoringResult.lastSeen.msecsTo(QDateTime::currentDateTime()) > 60000) {
|
||||
m_monitoringResult.online = false;
|
||||
emit monitorStateChanged(m_monitoringResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool status = sendDiscoveryMessage();
|
||||
if (!status) {
|
||||
delete m_monitorTimer;
|
||||
m_monitorTimer = nullptr;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
SolarmanDiscovery::DiscoveryResult SolarmanDiscovery::monitorResult() const
|
||||
{
|
||||
return m_monitoringResult;
|
||||
}
|
||||
|
||||
void SolarmanDiscovery::onReadyRead()
|
||||
{
|
||||
while (m_udp->hasPendingDatagrams()) {
|
||||
QByteArray data;
|
||||
data.resize(m_udp->pendingDatagramSize());
|
||||
QHostAddress host;
|
||||
m_udp->readDatagram(data.data(), m_udp->pendingDatagramSize(), &host);
|
||||
if (data == "WIFIKIT-214028-READ") {
|
||||
continue;
|
||||
}
|
||||
qCDebug(dcSolarman) << "UDP datagram from" << host.toString() << data;
|
||||
|
||||
QStringList parts = QString(data).split(",");
|
||||
if (parts.count() != 3) {
|
||||
qCDebug(dcSolarman()) << "Unexpected discovery reply format:" << data;
|
||||
continue;
|
||||
}
|
||||
DiscoveryResult result;
|
||||
result.ip = parts.at(0);
|
||||
result.mac = parts.at(1);
|
||||
result.serial = parts.at(2);
|
||||
result.online = true;
|
||||
|
||||
// Sanity check if the claimed IP matches with the sender
|
||||
if (result.ip.toIPv4Address() != host.toIPv4Address()) {
|
||||
qCDebug(dcSolarman) << "Sender IP doesn't match claimed IP:" << result.ip << host;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m_discoveryTimer) {
|
||||
qCDebug(dcSolarman()) << "Found solarman device" << result.serial << "on" << result.ip;
|
||||
if (!m_discoveryResults.contains(result)) {
|
||||
m_discoveryResults.append(result);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_monitorTimer) {
|
||||
if (result.serial == m_monitoringResult.serial) {
|
||||
m_monitoringResult = result;
|
||||
m_monitoringResult.lastSeen = QDateTime::currentDateTime();
|
||||
emit monitorStateChanged(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool SolarmanDiscovery::initUdp()
|
||||
{
|
||||
if (m_udp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
m_udp = new QUdpSocket(this);
|
||||
connect(m_udp, &QUdpSocket::readyRead, this, &SolarmanDiscovery::onReadyRead);
|
||||
bool status = m_udp->bind(48899, QUdpSocket::ShareAddress);
|
||||
if (!status) {
|
||||
qCWarning(dcSolarman()) << "Unable to bind UDP port 48899. SolarmanDiscovery won't work.";
|
||||
delete m_udp;
|
||||
m_udp = nullptr;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
bool SolarmanDiscovery::sendDiscoveryMessage()
|
||||
{
|
||||
qCDebug(dcSolarman()) << "Discovering solarman inverters...";
|
||||
QByteArray discoveryString("WIFIKIT-214028-READ");
|
||||
int len = m_udp->writeDatagram(discoveryString, QHostAddress::Broadcast, 48899);
|
||||
return len == discoveryString.length();
|
||||
}
|
||||
52
solarman/solarmandiscovery.h
Normal file
52
solarman/solarmandiscovery.h
Normal file
@ -0,0 +1,52 @@
|
||||
#ifndef SOLARMANDISCOVERY_H
|
||||
#define SOLARMANDISCOVERY_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QHostAddress>
|
||||
#include <QTimer>
|
||||
#include <QUdpSocket>
|
||||
#include <QDateTime>
|
||||
|
||||
class SolarmanDiscovery : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
class DiscoveryResult {
|
||||
public:
|
||||
QString serial;
|
||||
QHostAddress ip;
|
||||
QString mac;
|
||||
bool online;
|
||||
QDateTime lastSeen;
|
||||
bool operator==(const DiscoveryResult &other) {
|
||||
return serial == other.serial;
|
||||
}
|
||||
};
|
||||
|
||||
explicit SolarmanDiscovery(QObject *parent = nullptr);
|
||||
|
||||
bool discover();
|
||||
|
||||
bool monitor(const QString &serial);
|
||||
DiscoveryResult monitorResult() const;
|
||||
|
||||
signals:
|
||||
void discoveryResults(const QList<DiscoveryResult> &results);
|
||||
void monitorStateChanged(const DiscoveryResult &result);
|
||||
|
||||
private slots:
|
||||
void onReadyRead();
|
||||
|
||||
private:
|
||||
bool initUdp();
|
||||
bool sendDiscoveryMessage();
|
||||
|
||||
QList<DiscoveryResult> m_discoveryResults;
|
||||
|
||||
QUdpSocket *m_udp = nullptr;;
|
||||
DiscoveryResult m_monitoringResult;
|
||||
QTimer *m_discoveryTimer = nullptr;
|
||||
QTimer *m_monitorTimer = nullptr;
|
||||
};
|
||||
|
||||
#endif // SOLARMANDISCOVERY_H
|
||||
305
solarman/solarmanmodbus.cpp
Normal file
305
solarman/solarmanmodbus.cpp
Normal file
@ -0,0 +1,305 @@
|
||||
#include "solarmanmodbus.h"
|
||||
#include "solarmanmodbusreply.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QHostAddress>
|
||||
#include <QTimer>
|
||||
#include <QLoggingCategory>
|
||||
#include <QDateTime>
|
||||
#include <QTimeZone>
|
||||
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcSolarman)
|
||||
|
||||
#define MOCK_DATA 0
|
||||
|
||||
quint8 START_OF_MESSAGE = 0xA5;
|
||||
quint8 END_OF_MESSAGE = 0x15;
|
||||
quint8 FRAME_TYPE = 0x02;
|
||||
quint16 requestCode = 0x4510;
|
||||
quint16 responseCode = 0x1510;
|
||||
|
||||
SolarmanModbus::SolarmanModbus(QObject *parent)
|
||||
: QObject{parent}
|
||||
{
|
||||
m_socket = new QTcpSocket(this);
|
||||
connect(m_socket, &QTcpSocket::stateChanged, this, [=](QAbstractSocket::SocketState state){
|
||||
qCDebug(dcSolarman()) << "Socket state changed:" << state;
|
||||
if (state == QAbstractSocket::ConnectedState) {
|
||||
emit connectedChanged(true);
|
||||
} else if (state == QAbstractSocket::UnconnectedState){
|
||||
emit connectedChanged(false);
|
||||
}
|
||||
}, Qt::QueuedConnection); // Otherwise socket->isOpen() may stll be true if the user reconnects right away.
|
||||
|
||||
connect(m_socket, &QTcpSocket::readyRead, this, [this](){
|
||||
QByteArray data = m_socket->readAll();
|
||||
processData(data);
|
||||
});
|
||||
}
|
||||
|
||||
void SolarmanModbus::connectToHost(const QHostAddress &host, quint16 port, const QString &serial)
|
||||
{
|
||||
if (m_serial != serial || m_host != host || m_port != port) {
|
||||
m_serial = serial;
|
||||
m_host = host;
|
||||
m_port = port;
|
||||
m_socket->close();
|
||||
}
|
||||
|
||||
if (!m_socket->isOpen()) {
|
||||
qCDebug(dcSolarman()) << "Connecting to" << host.toString() << port;
|
||||
m_socket->connectToHost(host, port);
|
||||
} else {
|
||||
qCDebug(dcSolarman()) << "Still connected. Not reconnecting";
|
||||
}
|
||||
}
|
||||
|
||||
void SolarmanModbus::disconnectFromHost()
|
||||
{
|
||||
m_serial.clear();
|
||||
m_socket->disconnectFromHost();
|
||||
emit connectedChanged(false);
|
||||
}
|
||||
|
||||
bool SolarmanModbus::isConnected() const
|
||||
{
|
||||
return m_socket->isOpen();
|
||||
}
|
||||
|
||||
SolarmanModbusReply *SolarmanModbus::readRegisters(int slaveId, quint16 startRegister, quint16 endRegister, FunctionCode functionCode)
|
||||
{
|
||||
|
||||
#if MOCK_DATA == 1
|
||||
quint8 requestId = 0;
|
||||
SolarmanModbusReply *reply = new SolarmanModbusReply(requestId, slaveId, startRegister, endRegister, functionCode, this);
|
||||
|
||||
QByteArray mockData = QByteArray::fromHex(
|
||||
// Late afternoon
|
||||
"a5f30010150033fdc5fff40201aaab35004b0600001e2dba620103e0010002013232303230393036353500010000120c070000000112020700000bb800000101004b0000003c160807111210000000000abe07081450128e000000000000139c002c000000000000000000640000000000010000000000010000000000010000000000000000000000000000000000000004001100000000042a00000011000000000000042a0000000000000938000000000000000000001388000000000000000000000000042e00000000000014d20000000000000000000000000000000000000000000000000000000000000000000000000174001c0000000000356b15"
|
||||
// End of day
|
||||
// "a5f3001015002bfdc5fff402011a2900005a0600006adcef620103e0010002013232303230393036353500010000120c070000000112020700000bb800000101004b0000003c160807141d08000000000abe07081450128e000000000000139c002c000000000000000000640000000000010000000000010000000000010000000000000000000000000000000000000004001300000000042c00000013000000000000042c0000000000000924000000000000000000001388000000000000000000000000001e000000000000106800000000000000000000000000000000000000000000000000000000000000000000000001460000000000008ce6c615"
|
||||
);
|
||||
m_pendingReplies.append(reply);
|
||||
QMetaObject::invokeMethod(this, "processData", Qt::QueuedConnection, Q_ARG(QByteArray, mockData));
|
||||
#else
|
||||
|
||||
quint8 requestId = m_requestIdCounter++;
|
||||
SolarmanModbusReply *reply = new SolarmanModbusReply(requestId, slaveId, startRegister, endRegister, functionCode, this);
|
||||
|
||||
if (!m_socket->isOpen()) {
|
||||
reply->finish(false);
|
||||
return reply;
|
||||
}
|
||||
m_pendingReplies.append(reply);
|
||||
connect(reply, &SolarmanModbusReply::finished, this, [this, reply](){
|
||||
m_pendingReplies.removeAll(reply);
|
||||
});
|
||||
QByteArray request = createRequest(requestId, slaveId, startRegister, endRegister, functionCode, m_serial);
|
||||
qCDebug(dcSolarman) << "Requesting:" << request.toHex();
|
||||
m_socket->write(request);
|
||||
#endif
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
void SolarmanModbus::processData(const QByteArray &data)
|
||||
{
|
||||
qCDebug(dcSolarman) << "Data received:" << data.toHex();
|
||||
|
||||
if (!validateChecksum(data)) {
|
||||
qCWarning(dcSolarman()) << "Checksum verification failed for payload:" << data.toHex();
|
||||
return;
|
||||
}
|
||||
|
||||
QDataStream stream(data);
|
||||
stream.setByteOrder(QDataStream::LittleEndian);
|
||||
quint8 startOfMessage;
|
||||
stream >> startOfMessage;
|
||||
if (startOfMessage != START_OF_MESSAGE) {
|
||||
qCWarning(dcSolarman()) << "Skipping message... No Start of message found.";
|
||||
return;
|
||||
}
|
||||
quint16 payloadLength;
|
||||
stream >> payloadLength;
|
||||
qCDebug(dcSolarman()) << "Payload length:" << payloadLength;
|
||||
quint16 commandCode;
|
||||
stream >> commandCode;
|
||||
|
||||
quint8 requestId;
|
||||
stream >> requestId;
|
||||
quint8 packetCounter;
|
||||
stream >> packetCounter; // Probably for deduplication, dunno, just counts up to 0x6a (100) and resets to 1...
|
||||
|
||||
|
||||
if (commandCode != responseCode) {
|
||||
qCWarning(dcSolarman()) << "Unknown packet received:" << data.toHex();
|
||||
disconnectFromHost();
|
||||
return;
|
||||
QByteArray rsp;
|
||||
QDataStream rs(&rsp, QIODevice::WriteOnly);
|
||||
stream.setByteOrder(QDataStream::LittleEndian);
|
||||
rs << static_cast<quint8>(START_OF_MESSAGE);
|
||||
rs << static_cast<quint16>(1); // len
|
||||
rs << static_cast<quint16>(0x1710);
|
||||
rs << requestId;
|
||||
rs << packetCounter;
|
||||
QByteArray serialHex = getSerialHex(m_serial);
|
||||
qDebug() << "Serial hex:" << serialHex.toHex();
|
||||
rs.writeRawData(serialHex.data(), serialHex.length());
|
||||
rs << static_cast<quint8>(0);
|
||||
rs << createChecksum(rsp);
|
||||
rs << static_cast<quint8>(END_OF_MESSAGE);
|
||||
m_socket->write(rsp);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
char serialStr[4];
|
||||
stream.readRawData(serialStr, 4);
|
||||
QByteArray serial(serialStr, 4);
|
||||
|
||||
if (serial != getSerialHex(m_serial)) {
|
||||
qCWarning(dcSolarman()) << "Serial number does not match:" << serial.toHex() << getSerialHex(m_serial).toHex();
|
||||
return;
|
||||
}
|
||||
|
||||
// Skipping 10 unknown characters
|
||||
quint8 frameType;
|
||||
stream >> frameType;
|
||||
|
||||
quint8 sensorType;
|
||||
stream >> sensorType;
|
||||
|
||||
quint32 deliveryTime;
|
||||
stream >> deliveryTime;
|
||||
|
||||
quint32 powerOnTime;
|
||||
stream >> powerOnTime;
|
||||
// char unknownStr[10];
|
||||
// stream.readRawData(unknownStr, 10);
|
||||
|
||||
quint32 timestamp;
|
||||
stream >> timestamp;
|
||||
qCDebug(dcSolarman()) << "Device time:" << QDateTime::fromMSecsSinceEpoch((qulonglong)timestamp * 1000, QTimeZone::utc());
|
||||
|
||||
quint8 slaveId;
|
||||
stream >> slaveId;
|
||||
|
||||
quint8 rt;
|
||||
stream >> rt;
|
||||
FunctionCode functionCode = static_cast<FunctionCode>(rt);
|
||||
|
||||
quint8 registersLength;
|
||||
stream >> registersLength;
|
||||
|
||||
char dataStr[registersLength];
|
||||
stream.readRawData(dataStr, registersLength);
|
||||
QByteArray registers(dataStr, registersLength);
|
||||
|
||||
quint16 modbusCRC;
|
||||
stream >> modbusCRC;
|
||||
// While the checksum on the outer package is quite crappy (I've seen it colliding easily), we still don't bother checking the modbus CRC if that matches...
|
||||
|
||||
qCDebug(dcSolarman()) << "Reply received: SlaveID" << slaveId << functionCode << "payload len" << registersLength << "poweruptime" << powerOnTime << "deliverytime" << deliveryTime << "frametype" << frameType;
|
||||
|
||||
foreach (SolarmanModbusReply *reply, m_pendingReplies) {
|
||||
if (reply->requestId() == requestId) {
|
||||
reply->finish(true, registers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray SolarmanModbus::createRequest(quint8 requestId, quint8 slaveId, quint16 startRegister, quint16 endRegister, FunctionCode functionCode, const QString &serialNumber) {
|
||||
|
||||
|
||||
QByteArray packet;
|
||||
QDataStream stream(&packet, QIODevice::WriteOnly);
|
||||
stream.setByteOrder(QDataStream::LittleEndian);
|
||||
stream << START_OF_MESSAGE;
|
||||
QByteArray modbusPayload = createModbusPayload(slaveId, startRegister, endRegister, functionCode);
|
||||
stream << static_cast<quint16>(15 + modbusPayload.length());
|
||||
// stream.writeRawData((char*)CONTROL_CODE, 2);
|
||||
stream << requestCode;
|
||||
stream << requestId;
|
||||
stream << requestId;//static_cast<quint8>(0x00); // This seems to be a static counter counting up unrelated on both ends, but the inverter doesn't care if we don't use it.
|
||||
QByteArray serialHex = getSerialHex(serialNumber);
|
||||
qDebug() << "Serial hex:" << serialHex.toHex();
|
||||
stream.writeRawData(serialHex.data(), serialHex.length());
|
||||
stream << FRAME_TYPE;
|
||||
stream << static_cast<quint16>(0x0000); // Sensor type
|
||||
stream << static_cast<quint32>(0x00000000); // Deliverytime
|
||||
stream << static_cast<quint32>(0x00000000); // PowerOnTime
|
||||
stream << static_cast<quint32>(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch() / 1000);
|
||||
stream.writeRawData(modbusPayload.data(), modbusPayload.length());
|
||||
stream << createChecksum(packet);
|
||||
stream << END_OF_MESSAGE;
|
||||
return packet;
|
||||
}
|
||||
|
||||
QByteArray SolarmanModbus::createModbusPayload(quint8 slaveId, quint16 startRegister, quint16 endRegister, FunctionCode functionCode)
|
||||
{
|
||||
quint16 registerCount = endRegister - startRegister + 1;
|
||||
|
||||
QByteArray data;
|
||||
QDataStream stream(&data, QIODevice::WriteOnly);
|
||||
stream << static_cast<quint8>(slaveId);
|
||||
stream << static_cast<quint8>(functionCode);
|
||||
stream << startRegister;
|
||||
stream << registerCount;
|
||||
quint16 crc = createModbusCRC(data);
|
||||
stream.setByteOrder(QDataStream::LittleEndian);
|
||||
// stream.setByteOrder(QDataStream::BigEndian);
|
||||
stream << crc;
|
||||
return data;
|
||||
}
|
||||
|
||||
quint16 SolarmanModbus::createModbusCRC(const QByteArray &data)
|
||||
{
|
||||
quint16 poly = 0xA001;
|
||||
QDataStream stream(data);
|
||||
quint16 crc = 0xFFFF;
|
||||
|
||||
while (!stream.atEnd()) {
|
||||
quint8 byte;
|
||||
stream >> byte;
|
||||
crc ^= byte;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (crc & 0x0001) {
|
||||
crc = (crc >> 1) ^ poly;
|
||||
} else {
|
||||
crc = crc >> 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
QByteArray SolarmanModbus::getSerialHex(const QString &serialNumber) {
|
||||
QByteArray serialHex = QByteArray::fromHex(QByteArray::number(serialNumber.toLongLong(), 16));
|
||||
QByteArray reversed;
|
||||
reversed.reserve(serialHex.size());
|
||||
for (int i = serialHex.size() - 1; i >= 0; i--) {
|
||||
reversed.append(serialHex.at(i));
|
||||
}
|
||||
return reversed;
|
||||
}
|
||||
|
||||
quint8 SolarmanModbus::createChecksum(const QByteArray &data)
|
||||
{
|
||||
quint16 checksum = 0;
|
||||
for (int i = 1; i < data.length(); i++) {
|
||||
checksum += (quint8)data.at(i);
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
|
||||
bool SolarmanModbus::validateChecksum(const QByteArray &packet) {
|
||||
quint16 checksum = 0;
|
||||
// Don't include the checksum and END OF MESSAGE (-2)
|
||||
for (int i = 1; i < packet.length() - 2; i++) {
|
||||
checksum += packet[i];
|
||||
}
|
||||
quint8 final = static_cast<quint8>(checksum);
|
||||
return final == (quint8)packet.at(packet.length() - 2);
|
||||
}
|
||||
59
solarman/solarmanmodbus.h
Normal file
59
solarman/solarmanmodbus.h
Normal file
@ -0,0 +1,59 @@
|
||||
#ifndef SOLARMANMODBUS_H
|
||||
#define SOLARMANMODBUS_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QHostAddress>
|
||||
#include <QTcpSocket>
|
||||
|
||||
class SolarmanModbusReply;
|
||||
|
||||
class SolarmanModbus : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum FunctionCode {
|
||||
Invalid = 0x00,
|
||||
ReadCoilStatus = 0x01,
|
||||
ReadInputStatus = 0x02,
|
||||
ReadHoldingRegisters = 0x03,
|
||||
ReadInputRegisters = 0x04,
|
||||
ForceSingleCoil = 0x05,
|
||||
ForceSingleRegister = 0x06,
|
||||
ForceMultipleCoils = 0x0F,
|
||||
PresetMultipleRegisters = 0x10
|
||||
};
|
||||
Q_ENUM(FunctionCode)
|
||||
|
||||
explicit SolarmanModbus(QObject *parent = nullptr);
|
||||
|
||||
void connectToHost(const QHostAddress &host, quint16 port, const QString &serial);
|
||||
void disconnectFromHost();
|
||||
bool isConnected() const;
|
||||
|
||||
SolarmanModbusReply* readRegisters(int slaveId, quint16 startRegister, quint16 endRegister, FunctionCode functionCode);
|
||||
|
||||
signals:
|
||||
void connectedChanged(bool connected);
|
||||
|
||||
private slots:
|
||||
void processData(const QByteArray &data);
|
||||
|
||||
private:
|
||||
QByteArray createRequest(quint8 requestId, quint8 slaveId, quint16 startRegister, quint16 endRegister, FunctionCode functionCode, const QString &serialNumber);
|
||||
QByteArray createModbusPayload(quint8 slaveId, quint16 startRegister, quint16 endRegister, FunctionCode functionCode);
|
||||
quint16 createModbusCRC(const QByteArray &data);
|
||||
QByteArray getSerialHex(const QString &serialNumber);
|
||||
|
||||
quint8 createChecksum(const QByteArray &data);
|
||||
bool validateChecksum(const QByteArray &data);
|
||||
|
||||
QString m_serial;
|
||||
QHostAddress m_host;
|
||||
quint16 m_port = 0;
|
||||
QTcpSocket *m_socket = nullptr;
|
||||
quint8 m_requestIdCounter = 0;
|
||||
|
||||
QList<SolarmanModbusReply*> m_pendingReplies;
|
||||
};
|
||||
|
||||
#endif // SOLARMANMODBUS_H
|
||||
94
solarman/solarmanmodbusreply.cpp
Normal file
94
solarman/solarmanmodbusreply.cpp
Normal file
@ -0,0 +1,94 @@
|
||||
#include "solarmanmodbusreply.h"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QDataStream>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(dcSolarman)
|
||||
|
||||
SolarmanModbusReply::SolarmanModbusReply(quint8 requestId, quint8 slaveId, quint16 startRegister, quint16 endRegister, SolarmanModbus::FunctionCode functionCode, QObject *parent)
|
||||
: QObject{parent},
|
||||
m_requestId(requestId),
|
||||
m_slaveId(slaveId),
|
||||
m_startRegister(startRegister),
|
||||
m_endRegister(endRegister),
|
||||
m_functionCode(functionCode)
|
||||
{
|
||||
QTimer::singleShot(20000, this, [this](){
|
||||
finish(false);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
quint8 SolarmanModbusReply::requestId() const
|
||||
{
|
||||
return m_requestId;
|
||||
}
|
||||
|
||||
quint8 SolarmanModbusReply::slaveId() const
|
||||
{
|
||||
return m_slaveId;
|
||||
}
|
||||
|
||||
quint16 SolarmanModbusReply::startRegister() const
|
||||
{
|
||||
return m_startRegister;
|
||||
}
|
||||
|
||||
quint16 SolarmanModbusReply::endRegister() const
|
||||
{
|
||||
return m_endRegister;
|
||||
}
|
||||
|
||||
quint16 SolarmanModbusReply::readRegister16(quint16 reg) const
|
||||
{
|
||||
int index = (reg - m_startRegister) * 2;
|
||||
if (index < 0 || index + 1 >= m_data.length()) {
|
||||
qCWarning(dcSolarman()) << "Register" << reg << "out of range:" << m_startRegister << "-" << m_endRegister;
|
||||
return 0;
|
||||
}
|
||||
QByteArray clipped = m_data.right(m_data.length() - (reg - m_startRegister) * 2);
|
||||
QDataStream stream(clipped);
|
||||
quint16 ret = 0;
|
||||
stream >> ret;
|
||||
return ret;
|
||||
}
|
||||
|
||||
quint32 SolarmanModbusReply::readRegister32(quint16 reg) const
|
||||
{
|
||||
int index = (reg - m_startRegister) * 2;
|
||||
if (index < 0 || index + 3 >= m_data.length()) {
|
||||
qCWarning(dcSolarman()) << "Register" << reg << "out of range:" << m_startRegister << "-" << m_endRegister;
|
||||
return 0;
|
||||
}
|
||||
QByteArray clipped = m_data.right(m_data.length() - (reg - m_startRegister) * 2);
|
||||
QDataStream stream(clipped);
|
||||
quint32 ret = 0;
|
||||
quint16 tmp;
|
||||
stream >> tmp;
|
||||
ret = tmp;
|
||||
stream >> tmp;
|
||||
ret += (tmp << 16);
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString SolarmanModbusReply::readRegisterString(quint16 reg, quint8 registerCount) const
|
||||
{
|
||||
int index = (reg - m_startRegister) * 2;
|
||||
if (index < 0 || (index + registerCount) * 2 >= m_data.length()) {
|
||||
return QString();
|
||||
}
|
||||
QByteArray string = m_data.right(m_data.length() - (reg - m_startRegister) * 2).left(registerCount * 2);
|
||||
return string;
|
||||
}
|
||||
|
||||
void SolarmanModbusReply::finish(bool success, const QByteArray &data)
|
||||
{
|
||||
if (m_finished) {
|
||||
return;
|
||||
}
|
||||
m_finished = true;
|
||||
m_success = success;
|
||||
m_data = data;
|
||||
QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection, Q_ARG(bool, success));
|
||||
}
|
||||
40
solarman/solarmanmodbusreply.h
Normal file
40
solarman/solarmanmodbusreply.h
Normal file
@ -0,0 +1,40 @@
|
||||
#ifndef SOLARMANMODBUSREPLY_H
|
||||
#define SOLARMANMODBUSREPLY_H
|
||||
|
||||
#include <QObject>
|
||||
#include "solarmanmodbus.h"
|
||||
|
||||
class SolarmanModbusReply : public QObject
|
||||
{
|
||||
friend class SolarmanModbus;
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SolarmanModbusReply(quint8 requestId, quint8 slaveId, quint16 startRegister, quint16 endRegister, SolarmanModbus::FunctionCode, QObject *parent = nullptr);
|
||||
|
||||
quint8 requestId() const;
|
||||
quint8 slaveId() const;
|
||||
quint16 startRegister() const;
|
||||
quint16 endRegister() const;
|
||||
|
||||
quint16 readRegister16(quint16 reg) const;
|
||||
quint32 readRegister32(quint16 reg) const;
|
||||
QString readRegisterString(quint16 reg, quint8 registerCount) const;
|
||||
|
||||
signals:
|
||||
void finished(bool success);
|
||||
|
||||
private:
|
||||
void finish(bool success, const QByteArray &data = QByteArray());
|
||||
|
||||
quint8 m_requestId = 0;
|
||||
quint8 m_slaveId = 0;
|
||||
quint16 m_startRegister = 0;
|
||||
quint16 m_endRegister = 0;
|
||||
SolarmanModbus::FunctionCode m_functionCode = SolarmanModbus::FunctionCode::Invalid;
|
||||
QByteArray m_data;
|
||||
bool m_success = false;
|
||||
bool m_finished = false;
|
||||
|
||||
};
|
||||
|
||||
#endif // SOLARMANMODBUSREPLY_H
|
||||
@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1">
|
||||
<context>
|
||||
<name>IntegrationPluginBosswerk</name>
|
||||
<message>
|
||||
<location filename="../integrationpluginbosswerk.cpp" line="82"/>
|
||||
<source>Please enter your login credentials.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../integrationpluginbosswerk.cpp" line="93"/>
|
||||
<source>An error happened in the network communication.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../integrationpluginbosswerk.cpp" line="108"/>
|
||||
<source>Failed to log in at the inverter.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>bosswerk</name>
|
||||
<message>
|
||||
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="33"/>
|
||||
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="36"/>
|
||||
<source>Bosswerk</source>
|
||||
<extracomment>The name of the vendor ({26ec1591-cc37-4ac1-b943-04844e002601})
|
||||
----------
|
||||
The name of the plugin bosswerk ({595b7759-336d-4677-a014-1b0fd11f45ea})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="39"/>
|
||||
<source>Connected</source>
|
||||
<extracomment>The name of the StateType ({b1a9bdf7-1c87-4c5d-b7e5-835697e7b7e5}) of ThingClass mix00</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="42"/>
|
||||
<source>Current power consumption</source>
|
||||
<extracomment>The name of the StateType ({044c26ce-67a7-4c81-99b1-4aa35285b109}) of ThingClass mix00</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="45"/>
|
||||
<source>MAC address</source>
|
||||
<extracomment>The name of the ParamType (ThingClass: mix00, Type: thing, ID: {6fbe5f08-3539-447d-9281-916abe9d8128})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="48"/>
|
||||
<source>MI-300/600</source>
|
||||
<extracomment>The name of the ThingClass ({31ee3e61-eb3f-470b-8957-293fe65f404d})</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="51"/>
|
||||
<source>Signal strength</source>
|
||||
<extracomment>The name of the StateType ({4187873d-50dd-4470-8bd1-2787436db84d}) of ThingClass mix00</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../../../build/nymea-plugins-Desktop-Debug/bosswerk/plugininfo.h" line="54"/>
|
||||
<source>Total produced energy</source>
|
||||
<extracomment>The name of the StateType ({4a596301-3a8d-41de-bc97-275d23c0e5cd}) of ThingClass mix00</extracomment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
Loading…
x
Reference in New Issue
Block a user