mirror of
https://github.com/nymea/nymea-plugins.git
synced 2026-10-24 14:55:39 +02:00
New Plugin: Solarman
This commit is contained in:
parent
860fbac0e8
commit
c771aee038
@ -53,18 +53,19 @@ PLUGIN_DIRS = \
|
|||||||
powerfox \
|
powerfox \
|
||||||
pushbullet \
|
pushbullet \
|
||||||
pushnotifications \
|
pushnotifications \
|
||||||
shelly \
|
|
||||||
solarlog \
|
|
||||||
systemmonitor \
|
|
||||||
reversessh \
|
reversessh \
|
||||||
senic \
|
senic \
|
||||||
serialportcommander \
|
serialportcommander \
|
||||||
sgready \
|
sgready \
|
||||||
|
shelly \
|
||||||
simpleheatpump \
|
simpleheatpump \
|
||||||
sma \
|
sma \
|
||||||
|
solarlog \
|
||||||
|
solarman \
|
||||||
somfytahoma \
|
somfytahoma \
|
||||||
sonos \
|
sonos \
|
||||||
sunposition \
|
sunposition \
|
||||||
|
systemmonitor \
|
||||||
tado \
|
tado \
|
||||||
tasmota \
|
tasmota \
|
||||||
tcpcommander \
|
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