diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 9265169f..24070d4f 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -118,6 +118,7 @@ isEmpty(WITH_PLUGINS) { } PLUGINS-=$${WITHOUT_PLUGINS} +#FIXME: PLUGINS=sma message("Building plugins:") for(plugin, PLUGINS) { exists($${plugin}) { diff --git a/sma/integrationpluginsma.cpp b/sma/integrationpluginsma.cpp index 0230ec27..cdf92103 100644 --- a/sma/integrationpluginsma.cpp +++ b/sma/integrationpluginsma.cpp @@ -31,13 +31,19 @@ #include "integrationpluginsma.h" #include "plugininfo.h" -#include "network/networkdevicediscovery.h" +#include +#include "speedwirediscovery.h" IntegrationPluginSma::IntegrationPluginSma() { } +void IntegrationPluginSma::init() +{ + +} + void IntegrationPluginSma::discoverThings(ThingDiscoveryInfo *info) { if (info->thingClassId() == sunnyWebBoxThingClassId) { @@ -84,18 +90,44 @@ void IntegrationPluginSma::discoverThings(ThingDiscoveryInfo *info) info->addThingDescriptors(descriptors); info->finish(Thing::ThingErrorNoError); }); - } else if (info->thingClassId() == speedwireInverterThingClassId) { - SpeedwireInterface *speedwireDiscovery = new SpeedwireInterface(info); + } else if (info->thingClassId() == speedwireMeterThingClassId) { + SpeedwireDiscovery *speedwireDiscovery = new SpeedwireDiscovery(hardwareManager()->networkDeviceDiscovery(), info); if (!speedwireDiscovery->initialize()) { qCWarning(dcSma()) << "Could not discovery inverter. The speedwire interface initialization failed."; info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Unable to discover the network.")); return; } - connect(speedwireDiscovery, &SpeedwireInterface::discoveryFinished, this, [=](){ + connect(speedwireDiscovery, &SpeedwireDiscovery::discoveryFinished, this, [=](){ qCDebug(dcSma()) << "Speed wire discovery finished."; - speedwireDiscovery->deleteLater(); + + ThingDescriptors descriptors; + foreach (const SpeedwireDiscovery::SpeedwireDiscoveryResult &result, speedwireDiscovery->discoveryResult()) { + if (result.deviceType == SpeedwireInterface::DeviceTypeMeter) { + if (result.serialNumber == 0) + continue; + + ThingDescriptor descriptor(speedwireMeterThingClassId, "SMA Energy Meter", "Serial: " + QString::number(result.serialNumber) + " - " + result.address.toString()); + // We found an energy meter, let's check if we already added this one + foreach (Thing *existingThing, myThings()) { + if (existingThing->paramValue(speedwireMeterThingSerialNumberParamTypeId).toUInt() == result.serialNumber) { + descriptor.setThingId(existingThing->id()); + break; + } + } + + ParamList params; + params << Param(speedwireMeterThingHostParamTypeId, result.address.toString()); + params << Param(speedwireMeterThingMacAddressParamTypeId, result.networkDeviceInfo.macAddress()); + params << Param(speedwireMeterThingSerialNumberParamTypeId, result.serialNumber); + params << Param(speedwireMeterThingModelIdParamTypeId, result.modelId); + descriptor.setParams(params); + descriptors.append(descriptor); + } + } + + info->addThingDescriptors(descriptors); info->finish(Thing::ThingErrorNoError); }); @@ -145,7 +177,49 @@ void IntegrationPluginSma::setupThing(ThingSetupInfo *info) connect(m_refreshTimer, &PluginTimer::timeout, this, &IntegrationPluginSma::onRefreshTimer); } }); - } else { + } else if (thing->thingClassId() == speedwireMeterThingClassId) { + QHostAddress address = QHostAddress(thing->paramValue(speedwireMeterThingHostParamTypeId).toString()); + quint32 serialNumber = static_cast(thing->paramValue(speedwireMeterThingSerialNumberParamTypeId).toUInt()); + quint16 modelId = static_cast(thing->paramValue(speedwireMeterThingModelIdParamTypeId).toUInt()); + + if (m_speedwireMeters.contains(thing)) { + m_speedwireMeters.take(thing)->deleteLater(); + } + + SpeedwireMeter *meter = new SpeedwireMeter(address, modelId, serialNumber, this); + if (!meter->initialize()) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + // TODO: reachable state + + connect(meter, &SpeedwireMeter::valuesUpdated, this, [=](){ + thing->setStateValue(speedwireMeterConnectedStateTypeId, true); + thing->setStateValue(speedwireMeterCurrentPowerStateTypeId, meter->currentPower()); + thing->setStateValue(speedwireMeterCurrentPowerPhaseAStateTypeId, meter->currentPowerPhaseA()); + thing->setStateValue(speedwireMeterCurrentPowerPhaseBStateTypeId, meter->currentPowerPhaseB()); + thing->setStateValue(speedwireMeterCurrentPowerPhaseCStateTypeId, meter->currentPowerPhaseC()); + thing->setStateValue(speedwireMeterVoltagePhaseAStateTypeId, meter->voltagePhaseA()); + thing->setStateValue(speedwireMeterVoltagePhaseBStateTypeId, meter->voltagePhaseB()); + thing->setStateValue(speedwireMeterVoltagePhaseCStateTypeId, meter->voltagePhaseC()); + thing->setStateValue(speedwireMeterTotalEnergyConsumedStateTypeId, meter->totalEnergyConsumed()); + thing->setStateValue(speedwireMeterTotalEnergyProducedStateTypeId, meter->totalEnergyProduced()); + thing->setStateValue(speedwireMeterEnergyConsumedPhaseAStateTypeId, meter->energyConsumedPhaseA()); + thing->setStateValue(speedwireMeterEnergyConsumedPhaseBStateTypeId, meter->energyConsumedPhaseB()); + thing->setStateValue(speedwireMeterEnergyConsumedPhaseCStateTypeId, meter->energyConsumedPhaseC()); + thing->setStateValue(speedwireMeterEnergyProducedPhaseAStateTypeId, meter->energyProducedPhaseA()); + thing->setStateValue(speedwireMeterEnergyProducedPhaseBStateTypeId, meter->energyProducedPhaseB()); + thing->setStateValue(speedwireMeterEnergyProducedPhaseCStateTypeId, meter->energyProducedPhaseC()); + thing->setStateValue(speedwireMeterCurrentPhaseAStateTypeId, meter->amperePhaseA()); + thing->setStateValue(speedwireMeterCurrentPhaseBStateTypeId, meter->amperePhaseB()); + thing->setStateValue(speedwireMeterCurrentPhaseCStateTypeId, meter->amperePhaseC()); + thing->setStateValue(speedwireMeterFirmwareVersionStateTypeId, meter->softwareVersion()); + }); + + m_speedwireMeters.insert(thing, meter); + info->finish(Thing::ThingErrorNoError); + }else { Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); } } diff --git a/sma/integrationpluginsma.h b/sma/integrationpluginsma.h index e2c77fd0..516a5e1b 100644 --- a/sma/integrationpluginsma.h +++ b/sma/integrationpluginsma.h @@ -35,7 +35,7 @@ #include "plugintimer.h" #include "sunnywebbox.h" -#include "speedwireinterface.h" +#include "speedwiremeter.h" class IntegrationPluginSma: public IntegrationPlugin { Q_OBJECT @@ -45,6 +45,7 @@ class IntegrationPluginSma: public IntegrationPlugin { public: explicit IntegrationPluginSma(); + void init() override; void discoverThings(ThingDiscoveryInfo *info) override; void setupThing(ThingSetupInfo *info) override; void postSetupThing(Thing *thing) override; @@ -59,6 +60,7 @@ private slots: private: PluginTimer *m_refreshTimer = nullptr; QHash m_sunnyWebBoxes; + QHash m_speedwireMeters; }; #endif // INTEGRATIONPLUGINSMA_H diff --git a/sma/integrationpluginsma.json b/sma/integrationpluginsma.json index de0a763d..b751c93d 100644 --- a/sma/integrationpluginsma.json +++ b/sma/integrationpluginsma.json @@ -26,7 +26,7 @@ { "id": "03f32361-4e13-4597-a346-af8d16a986b3", "name": "macAddress", - "displayName": "hardware address", + "displayName": "MAC address", "type": "QString", "inputType": "TextLine", "readOnly": true @@ -88,10 +88,10 @@ }, { "id": "0c5097af-e136-4430-9fb4-0ccbb30c3e1c", - "name": "speedwireInverter", - "displayName": "SMA Inverter Speedwire", + "name": "speedwireMeter", + "displayName": "SMA Energy Meter", "createMethods": ["discovery", "user"], - "interfaces": ["solarinverter"], + "interfaces": [ "energymeter" ], "paramTypes": [ { "id": "d90193e6-a996-4e49-bf6d-564d596d7e74", @@ -104,10 +104,26 @@ { "id": "2780eab7-1f1c-4cc7-a789-a8790329ca9e", "name": "macAddress", - "displayName": "hardware address", + "displayName": "MAC address", "type": "QString", "inputType": "TextLine", "readOnly": true + }, + { + "id": "7c81a0c5-9bc6-43bb-a01a-4de5fe656bba", + "name": "serialNumber", + "displayName": "Serial number", + "type": "QString", + "inputType": "TextLine", + "readOnly": true + }, + { + "id": "abdc114d-1fac-4454-8b82-871ed5cdf28c", + "name": "modelId", + "displayName": "Model ID", + "type": "uint", + "inputType": "TextLine", + "readOnly": true } ], "stateTypes": [ @@ -120,7 +136,61 @@ "defaultValue": false }, { - "id": "015868bd-cc35-44cf-b631-78ea7c73b967", + "id": "44ee2491-8376-41cd-a21d-185c736152ec", + "name": "voltagePhaseA", + "displayName": "Voltage phase A", + "displayNameEvent": "Voltage phase A changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "56ae3555-f874-4c2d-8833-17573dce477a", + "name": "voltagePhaseB", + "displayName": "Voltage phase B", + "displayNameEvent": "Voltage phase B changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "51cbb29b-29f0-480a-9d7d-b8f4e6a205ae", + "name": "voltagePhaseC", + "displayName": "Voltage phase C", + "displayNameEvent": "Voltage phase C changed", + "type": "double", + "unit": "Volt", + "defaultValue": 0 + }, + { + "id": "45bbdbef-1832-4870-bff5-299e580fb4da", + "name": "currentPhaseA", + "displayName": "Current phase A", + "displayNameEvent": "Current phase A changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "b3a4fdd2-b6b8-4c58-9da3-2084ad414022", + "name": "currentPhaseB", + "displayName": "Current phase B", + "displayNameEvent": "Current phase B changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "b3655188-3854-4336-ae3c-61d3bda6fc4d", + "name": "currentPhaseC", + "displayName": "Current phase C", + "displayNameEvent": "Current phase C changed", + "type": "double", + "unit": "Ampere", + "defaultValue": 0 + }, + { + "id": "d4ac7f37-e30a-44e4-93cb-ad16df18b8f1", "name": "currentPower", "displayName": "Current power", "displayNameEvent": "Current power changed", @@ -129,13 +199,111 @@ "defaultValue": 0 }, { - "id": "f29b6283-873b-45f5-8a14-622d34f11d4f", + "id": "c5d09c63-7461-4fb8-a6fe-bc7aa919be30", + "name": "currentPowerPhaseA", + "displayName": "Current power phase A", + "displayNameEvent": "Current power phase A changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "c52d4422-b521-4804-a7a7-c4398e91e760", + "name": "currentPowerPhaseB", + "displayName": "Current power phase B", + "displayNameEvent": "Current power phase B changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "555e892c-3ca7-4100-9832-6ac13b87eb04", + "name": "currentPowerPhaseC", + "displayName": "Current power phase C", + "displayNameEvent": "Current power phase C changed", + "type": "double", + "unit": "Watt", + "defaultValue": 0 + }, + { + "id": "4fb0a4c1-18ed-4d02-b6d0-c07e9b96a56d", + "name": "totalEnergyConsumed", + "displayName": "Total energy consumed", + "displayNameEvent": "Total energy consumed changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "76ca68d8-6781-4d2a-8663-440aec40b4de", "name": "totalEnergyProduced", "displayName": "Total energy produced", "displayNameEvent": "Total energy produced changed", "type": "double", "unit": "KiloWattHour", - "defaultValue": 0 + "defaultValue": 0.00 + }, + { + "id": "b4ff2c71-f81d-4904-bbac-0c0c6e8a5a33", + "name": "energyConsumedPhaseA", + "displayName": "Energy consumed phase A", + "displayNameEvent": "Energy consumed phase A changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "c4e5f569-ac5d-4761-a898-888880bfd59f", + "name": "energyConsumedPhaseB", + "displayName": "Energy consumed phase B", + "displayNameEvent": "Energy consumed phase B changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "aabc02d7-8dc3-4637-8bf2-dc2e0e737ad3", + "name": "energyConsumedPhaseC", + "displayName": "Energy consumed phase C", + "displayNameEvent": "Energy consumed phase C changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "754c3b67-768a-47f7-99d8-f66c198f0835", + "name": "energyProducedPhaseA", + "displayName": "Energy produced phase A", + "displayNameEvent": "Energy produced phase A changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "7eb08c45-24cf-40ce-be28-f3564f087672", + "name": "energyProducedPhaseB", + "displayName": "Energy produced phase B", + "displayNameEvent": "Energy produced phase B changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "1eb2bf01-5ec6-42e5-b348-ac1e95199d14", + "name": "energyProducedPhaseC", + "displayName": "Energy produced phase C", + "displayNameEvent": "Energy produced phase C changed", + "type": "double", + "unit": "KiloWattHour", + "defaultValue": 0.00 + }, + { + "id": "a685393c-8b7e-42c5-bb41-f9907c074626", + "name": "firmwareVersion", + "displayName": "Firmware version", + "displayNameEvent": "Firmware version changed", + "type": "QString", + "defaultValue": "" } ] } diff --git a/sma/obisdata.cpp b/sma/obisdata.cpp new file mode 100644 index 00000000..5866185f --- /dev/null +++ b/sma/obisdata.cpp @@ -0,0 +1,6 @@ +#include "obisdata.h" + +ObisData::ObisData() +{ + +} diff --git a/sma/obisdata.h b/sma/obisdata.h new file mode 100644 index 00000000..0ebe468e --- /dev/null +++ b/sma/obisdata.h @@ -0,0 +1,11 @@ +#ifndef OBISDATA_H +#define OBISDATA_H + + +class ObisData +{ +public: + ObisData(); +}; + +#endif // OBISDATA_H diff --git a/sma/sma.pro b/sma/sma.pro index 6a989d92..313dbae1 100644 --- a/sma/sma.pro +++ b/sma/sma.pro @@ -4,10 +4,16 @@ QT += network SOURCES += \ integrationpluginsma.cpp \ + obisdata.cpp \ + speedwirediscovery.cpp \ speedwireinterface.cpp \ + speedwiremeter.cpp \ sunnywebbox.cpp HEADERS += \ integrationpluginsma.h \ + obisdata.h \ + speedwirediscovery.h \ speedwireinterface.h \ + speedwiremeter.h \ sunnywebbox.h diff --git a/sma/speedwirediscovery.cpp b/sma/speedwirediscovery.cpp new file mode 100644 index 00000000..be92340a --- /dev/null +++ b/sma/speedwirediscovery.cpp @@ -0,0 +1,365 @@ +#include "speedwirediscovery.h" +#include "extern-plugininfo.h" + +#include +#include + +SpeedwireDiscovery::SpeedwireDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent) : + QObject(parent), + m_networkDeviceDiscovery(networkDeviceDiscovery) +{ + // More details: https://github.com/RalfOGit/libspeedwire/ + + + // //! Multicast device discovery request packet, according to SMA documentation. + // //! However, this does not seem to be supported anymore with version 3.x devices + // const unsigned char multicast_request[] = { + // 0x53, 0x4d, 0x41, 0x00, 0x00, 0x04, 0x02, 0xa0, // sma signature, tag0 + // 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x20, // 0xffffffff group, 0x0000 length, 0x0020 "SMA Net ?", Version ? + // 0x00, 0x00, 0x00, 0x00 // 0x0000 protocol, 0x00 #long words, 0x00 ctrl + // }; + + // //! Unicast device discovery request packet, according to SMA documentation + // const unsigned char unicast_request[] = { + // 0x53, 0x4d, 0x41, 0x00, 0x00, 0x04, 0x02, 0xa0, // sma signature, tag0 + // 0x00, 0x00, 0x00, 0x01, 0x00, 0x26, 0x00, 0x10, // 0x26 length, 0x0010 "SMA Net 2", Version 0 + // 0x60, 0x65, 0x09, 0xa0, 0xff, 0xff, 0xff, 0xff, // 0x6065 protocol, 0x09 #long words, 0xa0 ctrl, 0xffff dst susyID any, 0xffffffff dst serial any + // 0xff, 0xff, 0x00, 0x00, 0x7d, 0x00, 0x52, 0xbe, // 0x0000 dst cntrl, 0x007d src susy id, 0x3a28be52 src serial + // 0x28, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x0000 src cntrl, 0x0000 error code, 0x0000 fragment ID + // 0x01, 0x80, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, // 0x8001 packet ID + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 0x00, 0x00 + // }; + + // // Request: 534d4100000402a00000000100260010 606509a0 ffffffffffff0000 7d0052be283a0000 000000000180 00020000 00000000 00000000 00000000 => command = 0x00000200, first = 0x00000000; last = 0x00000000; trailer = 0x00000000 + // // Response 534d4100000402a000000001004e0010 606513a0 7d0052be283a00c0 7a01842a71b30000 000000000180 01020000 00000000 00000000 00030000 00ff0000 00000000 01007a01 842a71b3 00000a00 0c000000 00000000 00000000 01010000 00000000 + + + + + + + + + // qCDebug(dcSma()) << "SpeedwireDiscovery: Create speed wire interface for multicast" << m_multicastAddress.toString() << "on port" << m_port; + // QByteArray exampleData = QByteArray::fromHex("534d4100000402a000000001024400106069010e714369aee618a41600010400000000000001080000000021391229100002040000004415000208000000001575a137d800030400000000000003080000000003debed0e800040400000017c6000408000000001008c2070000090400000000000009080000000027c77bed20000a04000000481d000a08000000001722823410000d0400000003b00015040000000000001508000000000d1e1e0e3000160400000015120016080000000006c5a2d8b800170400000000000017080000000001bd6f680000180400000007990018080000000004def712b8001d040000000000001d08000000000eeefaafd0001e040000001666001e0800000000074b38bf88001f040000000a300020040000037bcb00210400000003ad0029040000000000002908000000000a9b1afec8002a040000001a81002a08000000000803e62b88002b040000000000002b080000000001511459b8002c0400000006d5002c0800000000052c8455b80031040000000000003108000000000cf83b37100032040000001b5f0032080000000008a6e257f80033040000000c3f003404000003747900350400000003c8003d040000000000003d08000000000a53d0ba08003e040000001482003e080000000007800fd188003f040000000000003f080000000001185820c8004004000000095800400800000000064563b1900045040000000000004508000000000d26d3eae0004604000000168900460800000000082b4fc5a80047040000000a440048040000037ed1004904000000038e900000000102085200000000"); + // processDatagram(QHostAddress("127.0.0.1"), m_port, exampleData); + + m_multicastSocket = new QUdpSocket(this); + connect(m_multicastSocket, &QUdpSocket::readyRead, this, &SpeedwireDiscovery::readPendingDatagramsMulticast); + connect(m_multicastSocket, &QUdpSocket::stateChanged, this, &SpeedwireDiscovery::onSocketStateChanged); + connect(m_multicastSocket, SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(onSocketError(QAbstractSocket::SocketError))); + + m_unicastSocket = new QUdpSocket(this); + connect(m_unicastSocket, &QUdpSocket::readyRead, this, &SpeedwireDiscovery::readPendingDatagramsUnicast); + connect(m_unicastSocket, &QUdpSocket::stateChanged, this, &SpeedwireDiscovery::onSocketStateChanged); + connect(m_unicastSocket, SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(onSocketError(QAbstractSocket::SocketError))); + + m_discoveryTimer.setInterval(1000); + m_discoveryTimer.setSingleShot(false); + connect(&m_discoveryTimer, &QTimer::timeout, this, &SpeedwireDiscovery::sendDiscoveryRequest); +} + +SpeedwireDiscovery::~SpeedwireDiscovery() +{ + if (m_initialized) { + if (!m_multicastSocket->leaveMulticastGroup(m_multicastAddress)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Failed to leave multicast group" << m_multicastAddress.toString(); + } + + m_multicastSocket->close(); + } +} + +bool SpeedwireDiscovery::initialize() +{ + m_multicastSocket->close(); + m_initialized = false; + + // Setup multicast socket + if (!m_multicastSocket->bind(QHostAddress::AnyIPv4, m_port, QAbstractSocket::ShareAddress | QAbstractSocket::ReuseAddressHint)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Initialization failed. Could not bind multicast socket to port" << m_port << m_multicastSocket->errorString(); + return false; + } + + if (!m_multicastSocket->joinMulticastGroup(m_multicastAddress)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Initialization failed. Could not join multicast group" << m_multicastAddress.toString() << m_multicastSocket->errorString(); + return false; + } + + // Setup unicast socket + if (!m_unicastSocket->bind(QHostAddress::AnyIPv4, m_port, QAbstractSocket::ShareAddress | QAbstractSocket::ReuseAddressHint)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Initialization failed. Could not bind to port" << m_port << m_multicastSocket->errorString(); + return false; + } + + qCDebug(dcSma()) << "SpeedwireDiscovery: Interface initialized successfully."; + m_initialized = true; + return m_initialized; +} + +bool SpeedwireDiscovery::initialized() const +{ + return m_initialized; +} + +bool SpeedwireDiscovery::startDiscovery() +{ + // 1. Discover all network devices + // 2. Send upd multicast and unicast messages to verify if it is a SMA speedwire device + + if (m_discoveryRunning) + return true; + + if (!m_initialized) { + qCDebug(dcSma()) << "SpeedwireDiscovery: Failed to start discovery because the socket has not been initialized successfully."; + return false; + } + + // CLean up + m_results.clear(); + m_networkDeviceInfos.clear(); + + qCDebug(dcSma()) << "SpeedwireDiscovery: Start discovering network..."; + NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover(); + connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){ + qCDebug(dcSma()) << "Discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "devices"; + m_networkDeviceInfos = discoveryReply->networkDeviceInfos(); + + foreach (const NetworkDeviceInfo &networkDeviceInfo, discoveryReply->networkDeviceInfos()) { + // 2. Send unicast to all results and start requesting on multicast address + sendUnicastDiscoveryRequest(networkDeviceInfo.address()); + } + + startMulticastDiscovery(); + }); + + return true; +} + +bool SpeedwireDiscovery::discoveryRunning() const +{ + return m_discoveryRunning; +} + +QList SpeedwireDiscovery::discoveryResult() const +{ + return m_results.values(); +} + +void SpeedwireDiscovery::startMulticastDiscovery() +{ + // Start sending multicast messages + sendDiscoveryRequest(); + + m_discoveryRunning = true; + QTimer::singleShot(5000, this, &SpeedwireDiscovery::onDiscoveryProcessFinished); + + m_discoveryTimer.start(); +} + +void SpeedwireDiscovery::sendUnicastDiscoveryRequest(const QHostAddress &targetHostAddress) +{ + if (m_unicastSocket->writeDatagram(m_discoveryDatagramUnicast, targetHostAddress, m_port) < 0) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Failed to send unicast discovery datagram to address" << targetHostAddress.toString(); + return; + } + + qCDebug(dcSma()) << "SpeedwireDiscovery: Sent successfully the discovery request to unicast address" << targetHostAddress.toString(); +} + +void SpeedwireDiscovery::readPendingDatagramsMulticast() +{ + QUdpSocket *socket = qobject_cast(sender()); + + QByteArray datagram; + QHostAddress senderAddress; + quint16 senderPort; + + while (socket->hasPendingDatagrams()) { + datagram.resize(socket->pendingDatagramSize()); + socket->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort); + qCDebug(dcSma()) << "SpeedwireDiscovery: Received multicast data from" << QString("%1:%2").arg(senderAddress.toString()).arg(senderPort); + //qCDebug(dcSma()) << "SpeedwireDiscovery: " << datagram.toHex(); + processDatagram(senderAddress, senderPort, datagram); + } +} + +void SpeedwireDiscovery::readPendingDatagramsUnicast() +{ + QUdpSocket *socket = qobject_cast(sender()); + + QByteArray datagram; + QHostAddress senderAddress; + quint16 senderPort; + + while (socket->hasPendingDatagrams()) { + datagram.resize(socket->pendingDatagramSize()); + socket->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort); + qCDebug(dcSma()) << "SpeedwireDiscovery: Received unicast data from" << QString("%1:%2").arg(senderAddress.toString()).arg(senderPort); + //qCDebug(dcSma()) << "SpeedwireDiscovery: " << datagram.toHex(); + processDatagram(senderAddress, senderPort, datagram); + } +} + + +void SpeedwireDiscovery::onSocketError(QAbstractSocket::SocketError error) +{ + qCDebug(dcSma()) << "SpeedwireDiscovery: Socket error" << error; +} + +void SpeedwireDiscovery::onSocketStateChanged(QAbstractSocket::SocketState socketState) +{ + qCDebug(dcSma()) << "SpeedwireDiscovery: Socket state changed" << socketState; +} + +void SpeedwireDiscovery::processDatagram(const QHostAddress &senderAddress, quint16 senderPort, const QByteArray &datagram) +{ + // Check min size of SMA datagrams + if (datagram.size() < 18) { + qCDebug(dcSma()) << "SpeedwireDiscovery: Received datagram is to short to be a SMA speedwire message. Ignoring data..."; + return; + } + + // Ignore discovery requests + if (datagram == m_discoveryDatagramMulticast || datagram == m_discoveryDatagramUnicast) + return; + + QDataStream stream(datagram); + stream.setByteOrder(QDataStream::BigEndian); + + SpeedwireInterface::SpeedwireHeader header = SpeedwireInterface::parseHeader(stream); + if (!header.isValid()) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Datagram header is not valid. Ignoring data..."; + return; + } + // Example data: + // 534d4100 0004 02a0 0000 0001 0244 0010 6069 010e 7143 69ae e618a416 00010400000000000001080000000021391229100002040000004415000208000000001575a137d800030400000000000003080000000003debed0e800040400000017c6000408000000001008c2070000090400000000000009080000000027c77bed20000a04000000481d000a08000000001722823410000d0400000003b00015040000000000001508000000000d1e1e0e3000160400000015120016080000000006c5a2d8b800170400000000000017080000000001bd6f680000180400000007990018080000000004def712b8001d040000000000001d08000000000eeefaafd0001e040000001666001e0800000000074b38bf88001f040000000a300020040000037bcb00210400000003ad0029040000000000002908000000000a9b1afec8002a040000001a81002a08000000000803e62b88002b040000000000002b080000000001511459b8002c0400000006d5002c0800000000052c8455b80031040000000000003108000000000cf83b37100032040000001b5f0032080000000008a6e257f80033040000000c3f003404000003747900350400000003c8003d040000000000003d08000000000a53d0ba08003e040000001482003e080000000007800fd188003f040000000000003f080000000001185820c8004004000000095800400800000000064563b1900045040000000000004508000000000d26d3eae0004604000000168900460800000000082b4fc5a80047040000000a440048040000037ed1004904000000038e900000000102085200000000 + + + + qCDebug(dcSma()) << "SpeedwireDiscovery: ======================= Header"; + qCDebug(dcSma()) << "SpeedwireDiscovery: Length:" << header.headerLength; + qCDebug(dcSma()) << "SpeedwireDiscovery: Tag0:" << header.tagType; + qCDebug(dcSma()) << "SpeedwireDiscovery: Tag0 version:" << header.tagVersion; + qCDebug(dcSma()) << "SpeedwireDiscovery: Group:" << header.group << (header.group == 1 ? "(default group)" : ""); + qCDebug(dcSma()) << "SpeedwireDiscovery: Data length:" << header.payloadLength << datagram.length(); + qCDebug(dcSma()) << "SpeedwireDiscovery: SMA Net 2 Version" << header.smaNet2Version; + qCDebug(dcSma()) << "SpeedwireDiscovery: Protocol ID" << header.protocolId; + + if (header.protocolId == SpeedwireInterface::ProtocolIdDiscoveryResponse) { + qCDebug(dcSma()) << "SpeedwireDiscovery: Received discovery response from" << QString("%1:%2").arg(senderAddress.toString()).arg(senderPort); + // Response: 534d4100 0004 02a0 0000 0001 0002 0000 0001 + // "192.168.178.25:9522" "534d4100 0004 02a0 0000 0001 0002 0000 0001 0004 0010 0001 0003 0004 0020 0000 0001 0004 0030 c0a8 b219 0004 0040 0000 0000 0002 0070 ef0c 00000000" + // "192.168.178.22:9522" "534d4100 0004 02a0 0000 0001 0002 0000 0001 0004 0010 0001 0001 0004 0020 0000 0001 0004 0030 c0a8 b216 0004 0040 0000 0001 00000000" + + if (!datagram.startsWith(m_discoveryResponseDatagram)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Received discovery reply but the message start does not match the required schema. Ignoring data..."; + return; + } + + if (!m_results.contains(senderAddress)) { + qCDebug(dcSma()) << "SpeedwireDiscovery: --> Found SMA device on" << senderAddress.toString(); + if (!m_networkDeviceInfos.hasHostAddress(senderAddress)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Found SMA using UDP discovery but the host is not in the network discovery result list. Not adding to results" << senderAddress.toString(); + return; + } + + + SpeedwireDiscoveryResult result; + result.address = senderAddress; + if (m_networkDeviceInfos.hasHostAddress(senderAddress)) { + result.networkDeviceInfo = m_networkDeviceInfos.get(senderAddress); + } + result.deviceType = SpeedwireInterface::DeviceTypeUnknown; + m_results.insert(senderAddress, result); + } else { + if (m_networkDeviceInfos.hasHostAddress(senderAddress)) { + m_results[senderAddress].networkDeviceInfo = m_networkDeviceInfos.get(senderAddress); + } + } + return; + } + + // We received SMA data, let's parse depending on the protocol id + + if (header.protocolId == SpeedwireInterface::ProtocolIdMeter) { + // Example: 010e 714369ae + quint16 modelId; + quint32 serialNumber; + stream >> modelId >> serialNumber; + qCDebug(dcSma()) << "SpeedwireDiscovery: ======================= Meter identifier"; + qCDebug(dcSma()) << "SpeedwireDiscovery: Model ID:" << modelId; + qCDebug(dcSma()) << "SpeedwireDiscovery: Serial number:" << serialNumber; + + if (!m_results.contains(senderAddress)) { + SpeedwireDiscoveryResult result; + result.address = senderAddress; + result.deviceType = SpeedwireInterface::DeviceTypeMeter; + m_results.insert(senderAddress, result); + } + + if (m_networkDeviceInfos.hasHostAddress(senderAddress)) { + m_results[senderAddress].networkDeviceInfo = m_networkDeviceInfos.get(senderAddress); + } + + m_results[senderAddress].modelId = modelId; + m_results[senderAddress].serialNumber = serialNumber; + } else if (header.protocolId == SpeedwireInterface::ProtocolIdInverter) { + quint16 modelId; + quint32 serialNumber; + stream >> modelId >> serialNumber; + qCDebug(dcSma()) << "SpeedwireDiscovery: ======================= Inverter identifier"; + qCDebug(dcSma()) << "SpeedwireDiscovery: Model ID:" << modelId; + qCDebug(dcSma()) << "SpeedwireDiscovery: Serial number:" << serialNumber; + + if (!m_results.contains(senderAddress)) { + SpeedwireDiscoveryResult result; + result.address = senderAddress; + result.deviceType = SpeedwireInterface::DeviceTypeInverter; + m_results.insert(senderAddress, result); + } + + if (m_networkDeviceInfos.hasHostAddress(senderAddress)) { + m_results[senderAddress].networkDeviceInfo = m_networkDeviceInfos.get(senderAddress); + } + + m_results[senderAddress].modelId = modelId; + m_results[senderAddress].serialNumber = serialNumber; + } else { + qCWarning(dcSma()) << "SpeedwireDiscovery: Unhandled data received" << datagram.toHex(); + return; + } + +} + +void SpeedwireDiscovery::sendDiscoveryRequest() +{ + if (m_multicastSocket->writeDatagram(m_discoveryDatagramMulticast, m_multicastAddress, m_port) < 0) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Failed to send discovery datagram to multicast address" << m_multicastAddress.toString(); + return; + } + + qCDebug(dcSma()) << "SpeedwireDiscovery: Sent successfully the discovery request to multicast address" << m_multicastAddress.toString(); +} + +void SpeedwireDiscovery::onDiscoveryProcessFinished() +{ + qCDebug(dcSma()) << "SpeedwireDiscovery: Discovey finished. Found" << m_results.count() << "SMA devices in the network"; + m_discoveryTimer.stop(); + m_discoveryRunning = false; + + foreach (const SpeedwireDiscoveryResult &result, m_results) { + qCDebug(dcSma()) << "SpeedwireDiscovery: ============================================"; + qCDebug(dcSma()) << "SpeedwireDiscovery: Device type:" << result.deviceType; + qCDebug(dcSma()) << "SpeedwireDiscovery: Address:" << result.address.toString(); + qCDebug(dcSma()) << "SpeedwireDiscovery: Hostname:" << result.networkDeviceInfo.hostName(); + qCDebug(dcSma()) << "SpeedwireDiscovery: MAC:" << result.networkDeviceInfo.macAddress(); + qCDebug(dcSma()) << "SpeedwireDiscovery: MAC manufacturer:" << result.networkDeviceInfo.macAddressManufacturer(); + qCDebug(dcSma()) << "SpeedwireDiscovery: Model ID:" << result.modelId; + qCDebug(dcSma()) << "SpeedwireDiscovery: Serial number:" << result.serialNumber; + } + + + emit discoveryFinished(); +} diff --git a/sma/speedwirediscovery.h b/sma/speedwirediscovery.h new file mode 100644 index 00000000..c1da61e2 --- /dev/null +++ b/sma/speedwirediscovery.h @@ -0,0 +1,75 @@ +#ifndef SPEEDWIREDISCOVERY_H +#define SPEEDWIREDISCOVERY_H + +#include +#include +#include + +#include + +#include "speedwireinterface.h" + +class SpeedwireDiscovery : public QObject +{ + Q_OBJECT +public: + typedef struct SpeedwireDiscoveryResult { + QHostAddress address; + NetworkDeviceInfo networkDeviceInfo; + SpeedwireInterface::DeviceType deviceType = SpeedwireInterface::DeviceTypeUnknown; + quint16 modelId = 0; + quint32 serialNumber = 0; + } SpeedwireDiscoveryResult; + + explicit SpeedwireDiscovery(NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr); + ~SpeedwireDiscovery(); + + bool initialize(); + bool initialized() const; + + bool startDiscovery(); + bool discoveryRunning() const; + + QList discoveryResult() const; + +signals: + void discoveryFinished(); + +private: + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + QUdpSocket *m_multicastSocket = nullptr; + QUdpSocket *m_unicastSocket = nullptr; + QHostAddress m_multicastAddress = QHostAddress("239.12.255.254"); + quint16 m_port = 9522; + bool m_initialized = false; + + // Discovery + QTimer m_discoveryTimer; + bool m_discoveryRunning = false; + NetworkDeviceInfos m_networkDeviceInfos; + QHash m_results; + + // Static discovery datagrams for speedwire + QByteArray m_discoveryDatagramMulticast = QByteArray::fromHex("534d4100000402a0ffffffff0000002000000000"); + QByteArray m_discoveryResponseDatagram = QByteArray::fromHex("534d4100000402A000000001000200000001"); + + QByteArray m_discoveryDatagramUnicast = QByteArray::fromHex("534d4100000402a00000000100260010606509a0ffffffffffff00007d0052be283a000000000000018000020000000000000000000000000000"); + + void startMulticastDiscovery(); + void sendUnicastDiscoveryRequest(const QHostAddress &targetHostAddress); + +private slots: + void readPendingDatagramsMulticast(); + void readPendingDatagramsUnicast(); + void onSocketError(QAbstractSocket::SocketError error); + void onSocketStateChanged(QAbstractSocket::SocketState socketState); + + void processDatagram(const QHostAddress &senderAddress, quint16 senderPort, const QByteArray &datagram); + + void sendDiscoveryRequest(); + + void onDiscoveryProcessFinished(); + +}; + +#endif // SPEEDWIREDISCOVERY_H diff --git a/sma/speedwireinterface.cpp b/sma/speedwireinterface.cpp index a7b42b32..e30db2f8 100644 --- a/sma/speedwireinterface.cpp +++ b/sma/speedwireinterface.cpp @@ -1,49 +1,85 @@ #include "speedwireinterface.h" #include "extern-plugininfo.h" -#include - -SpeedwireInterface::SpeedwireInterface(QObject *parent) : - QObject(parent) +SpeedwireInterface::SpeedwireInterface(const QHostAddress &address, bool multicast, QObject *parent) : + QObject(parent), + m_address(address), + m_multicast(multicast) { m_socket = new QUdpSocket(this); connect(m_socket, &QUdpSocket::readyRead, this, &SpeedwireInterface::readPendingDatagrams); connect(m_socket, &QUdpSocket::stateChanged, this, &SpeedwireInterface::onSocketStateChanged); connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)),this, SLOT(onSocketError(QAbstractSocket::SocketError))); + + + + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000480 00020058 001e8200 ff208200 00000000 => query device type + // Response 534d4100000402a000000001009e0010 606527a0 7d0042be283a00a1 7a01842a71b30001 000000000480 01020058 01000000 03000000 011e8210 6f89e95f 534e3a20 33303130 35333831 31360000 00000000 00000000 00000000 00000000 + // 011f8208 6f89e95f 411f0001 feffff00 00000000 00000000 00000000 00000000 00000000 00000000 => 1f41 solar inverter + // 01208208 6f89e95f 96240000 80240000 81240001 82240000 feffff00 00000000 00000000 00000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000480 00028053 001e2500 ff1e2500 00000000 => query spot dc power + // Response 534d4100000402a000000001005e0010 606517a0 7d0042be283a00a1 7a01842a71b30001 000000000480 01028053 00000000 01000000 011e2540 61a7e95f 57000000 57000000 57000000 57000000 01000000 + // 021e2540 61a7e95f 5e000000 5e000000 5e000000 5e000000 01000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000580 00028053 001f4500 ff214500 00000000 => query spot dc voltage/current + // Response 534d4100000402a00000000100960010 606525a0 7d0042be283a00a1 7a01842a71b30001 000000000580 01028053 02000000 05000000 011f4540 61a7e95f 05610000 05610000 05610000 05610000 01000000 + // 021f4540 61a7e95f 505b0000 505b0000 505b0000 505b0000 01000000 + // 01214540 61a7e95f 60010000 60010000 60010000 60010000 01000000 + // 02214540 61a7e95f 95010000 95010000 95010000 95010000 01000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000680 00020051 00404600 ff424600 00000000 => query spot ac power + // Response 534d4100000402a000000001007a0010 60651ea0 7d0042be283a00a1 7a01842a71b30001 000000000680 01020051 09000000 0b000000 01404640 61a7e95f 38000000 38000000 38000000 38000000 01000000 + // 01414640 61a7e95f 37000000 37000000 37000000 37000000 01000000 + // 01424640 61a7e95f 39000000 39000000 39000000 39000000 01000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000780 00020051 00484600 ff554600 00000000 => query spot ac voltage/current + // Response 534d4100000402a000000001013e0010 60654fa0 7d0042be283a00a1 7a01842a71b30001 000000000780 01020051 0c000000 15000000 01484600 61a7e95f 5a590000 5a590000 5a590000 5a590000 01000000 + // 01494600 61a7e95f cf590000 cf590000 cf590000 cf590000 01000000 + // 014a4600 61a7e95f 7a590000 7a590000 7a590000 7a590000 01000000 + // 014b4600 61a7e95f f19a0000 f19a0000 f19a0000 f19a0000 01000000 + // 014c4600 61a7e95f 3c9b0000 3c9b0000 3c9b0000 3c9b0000 01000000 + // 014d4600 61a7e95f 189b0000 189b0000 189b0000 189b0000 01000000 + // 014e4600 51a7e95f 1d000000 1d000000 1d000000 1d000000 01000000 + // 01534640 61a7e95f 24010000 24010000 24010000 24010000 01000000 + // 01544640 61a7e95f 1e010000 1e010000 1e010000 1e010000 01000000 + // 01554640 61a7e95f 23010000 23010000 23010000 23010000 01000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000980 00028051 00482100 ff482100 00000000 => query device status + // Response 534d4100000402a000000001004e0010 606513a0 7d0042be283a00a1 7a01842a71b30001 000000000980 01028051 00000000 00000000 01482108 59c5e95f 33010001 feffff00 00000000 00000000 00000000 00000000 00000000 00000000 00000000 + // Request 534d4100000402a00000000100260010 606509a0 7a01842a71b30001 7d0042be283a0001 000000000a80 00028051 00644100 ff644100 00000000 => query grid relay status + // Response 534d4100000402a000000001004e0010 606513a0 7d0042be283a00a1 7a01842a71b30001 000000000a80 01028051 07000000 07000000 01644108 59c5e95f 33000001 37010000 fdffff00 feffff00 00000000 00000000 00000000 00000000 00000000 } SpeedwireInterface::~SpeedwireInterface() { - if (m_initialized) { - if (!m_socket->leaveMulticastGroup(m_multicastAddress)) { - qCWarning(dcSma()) << "SpeedwireInterface: Failed to leave multicast group" << m_multicastAddress.toString(); - } - - m_socket->close(); - } + deinitialize(); } bool SpeedwireInterface::initialize() { - // If we already initialized the socket, we are done - if (m_initialized) - return true; - - m_socket->close(); - m_initialized = false; - - if (!m_socket->bind(QHostAddress::AnyIPv4, m_port, QAbstractSocket::ShareAddress)) { - qCWarning(dcSma()) << "SpeedwireInterface: Cannot bind to port" << m_port << m_socket->errorString(); + if (!m_socket->bind(QHostAddress::AnyIPv4, m_port, QAbstractSocket::ShareAddress | QAbstractSocket::ReuseAddressHint)) { + qCWarning(dcSma()) << "SpeedwireInterface: Initialization failed. Could not bind to port" << m_port; return false; } - if (!m_socket->joinMulticastGroup(m_multicastAddress)) { - qCWarning(dcSma()) << "SpeedwireInterface: Failed to join multicast group" << m_multicastAddress.toString() << m_socket->errorString(); + if (m_multicast && !m_socket->joinMulticastGroup(m_multicastAddress)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Initialization failed. Could not join multicast group" << m_multicastAddress.toString() << m_socket->errorString(); return false; } + qCDebug(dcSma()) << "SpeedwireInterface: Interface initialized successfully."; m_initialized = true; - return true; + return m_initialized; +} + +void SpeedwireInterface::deinitialize() +{ + if (m_initialized) { + if (m_multicast) { + if (!m_socket->leaveMulticastGroup(m_multicastAddress)) { + qCWarning(dcSma()) << "SpeedwireDiscovery: Failed to leave multicast group" << m_multicastAddress.toString(); + } + + m_socket->close(); + m_initialized = false; + } + } } bool SpeedwireInterface::initialized() const @@ -51,52 +87,50 @@ bool SpeedwireInterface::initialized() const return m_initialized; } -bool SpeedwireInterface::startDiscovery() +quint16 SpeedwireInterface::sourceModelId() const { - if (m_discoveryRunning) - return true; - - qCDebug(dcSma()) << "SpeedwireInterface: Start discovering network..."; - if (!m_initialized) { - qCDebug(dcSma()) << "SpeedwireInterface: Failed to start discovery because the socket has not been initialized successfully."; - return false; - } - - // Discovery message - QByteArray discoveryDatagram = QByteArray::fromHex("534d4100000402a0ffffffff0000002000000000"); - if (m_socket->write(discoveryDatagram) < 0) { - qCWarning(dcSma()) << "SpeedwireInterface: Failed to send discovery datagram to multicast address" << m_multicastAddress.toString(); - return false; - } - - m_discoveryRunning = true; - QTimer::singleShot(10000, this, [=](){ - qCDebug(dcSma()) << "SpeedwireInterface: Discovey finished."; - m_discoveryRunning = false; - emit discoveryFinished(); - }); - - return true; + return m_sourceModelId; } -bool SpeedwireInterface::discoveryRunning() const +quint32 SpeedwireInterface::sourceSerialNumber() const { - return m_discoveryRunning; + return m_sourceSerialNumber; +} + +SpeedwireInterface::SpeedwireHeader SpeedwireInterface::parseHeader(QDataStream &stream) +{ + SpeedwireHeader header; + quint16 protocolId; + stream >> header.smaSignature >> header.headerLength; + stream >> header.tagType >> header.tagVersion >> header.group; + stream >> header.payloadLength >> header.smaNet2Version; + stream >> protocolId; + header.protocolId = static_cast(protocolId); + return header; +} + +void SpeedwireInterface::sendData(const QByteArray &data) +{ + m_socket->writeDatagram(data, m_address, m_port); } void SpeedwireInterface::readPendingDatagrams() { - QUdpSocket *socket= qobject_cast(sender()); - QByteArray datagram; QHostAddress senderAddress; quint16 senderPort; - while (socket->hasPendingDatagrams()) { - datagram.resize(socket->pendingDatagramSize()); - socket->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort); - qCDebug(dcSma()) << "SpeedwireInterface: Data received from" << senderAddress.toString() << datagram.toHex(); - //emit datagramReceived(senderAddress, datagram); + while (m_socket->hasPendingDatagrams()) { + datagram.resize(m_socket->pendingDatagramSize()); + m_socket->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort); + + // Process only data coming from our target address + if (senderAddress != m_address) + continue; + + qCDebug(dcSma()) << "SpeedwireInterface: Received data from" << QString("%1:%2").arg(senderAddress.toString()).arg(senderPort); + qCDebug(dcSma()) << "SpeedwireInterface: " << datagram.toHex(); + emit dataReceived(datagram); } } diff --git a/sma/speedwireinterface.h b/sma/speedwireinterface.h index 31df61b9..e619a803 100644 --- a/sma/speedwireinterface.h +++ b/sma/speedwireinterface.h @@ -3,29 +3,81 @@ #include #include +#include + + + class SpeedwireInterface : public QObject { Q_OBJECT public: - explicit SpeedwireInterface(QObject *parent = nullptr); + enum ProtocolId { + ProtocolIdUnknown = 0x0000, + ProtocolIdMeter = 0x6069, + ProtocolIdInverter = 0x6065, + ProtocolIdDiscoveryResponse = 0x0001, + ProtocolIdDiscovery = 0xffff + }; + Q_ENUM(ProtocolId) + + enum DeviceType { + DeviceTypeUnknown, + DeviceTypeMeter, + DeviceTypeInverter + }; + Q_ENUM(DeviceType) + + class SpeedwireHeader + { + public: + SpeedwireHeader() = default; + quint32 smaSignature = 0; + quint16 headerLength = 0; + quint16 tagType = 0; + quint16 tagVersion = 0; + quint16 group = 0; + quint16 payloadLength = 0; + quint16 smaNet2Version = 0; + ProtocolId protocolId = ProtocolIdUnknown; + + inline bool isValid() const { + return smaSignature == 0x534d4100 && protocolId != ProtocolIdUnknown; + } + + }; + + + explicit SpeedwireInterface(const QHostAddress &address, bool multicast, QObject *parent = nullptr); ~SpeedwireInterface(); bool initialize(); + void deinitialize(); + bool initialized() const; - bool startDiscovery(); - bool discoveryRunning() const; + quint16 sourceModelId() const; + quint32 sourceSerialNumber() const; + + static SpeedwireInterface::SpeedwireHeader parseHeader(QDataStream &stream); + +public slots: + void sendData(const QByteArray &data); signals: - void discoveryFinished(); + void dataReceived(const QByteArray &data); private: QUdpSocket *m_socket = nullptr; - QHostAddress m_multicastAddress = QHostAddress("239.12.255.254"); + QHostAddress m_address; quint16 m_port = 9522; + QHostAddress m_multicastAddress = QHostAddress("239.12.255.254"); + bool m_multicast = false; bool m_initialized = false; - bool m_discoveryRunning = false; + + // Requester + quint16 m_sourceModelId = 0x007d; + quint32 m_sourceSerialNumber = 0x3a28be42; private slots: void readPendingDatagrams(); diff --git a/sma/speedwiremeter.cpp b/sma/speedwiremeter.cpp new file mode 100644 index 00000000..330ec6f7 --- /dev/null +++ b/sma/speedwiremeter.cpp @@ -0,0 +1,270 @@ +#include "speedwiremeter.h" +#include "extern-plugininfo.h" + +SpeedwireMeter::SpeedwireMeter(const QHostAddress &address, quint16 modelId, quint32 serialNumber, QObject *parent) : + QObject(parent), + m_address(address), + m_modelId(modelId), + m_serialNumber(serialNumber) +{ + m_interface = new SpeedwireInterface(m_address, true, this); + connect(m_interface, &SpeedwireInterface::dataReceived, this, &SpeedwireMeter::processData); +} + +bool SpeedwireMeter::initialize() +{ + return m_interface->initialize(); +} + +bool SpeedwireMeter::initialized() const +{ + return m_interface->initialized(); +} + +double SpeedwireMeter::currentPower() const +{ + return m_currentPower; +} + +double SpeedwireMeter::totalEnergyProduced() const +{ + return m_totalEnergyProduced; +} + +double SpeedwireMeter::totalEnergyConsumed() const +{ + return m_totalEnergyConsumed; +} + +double SpeedwireMeter::energyConsumedPhaseA() const +{ + return m_energyConsumedPhaseA; +} + +double SpeedwireMeter::energyConsumedPhaseB() const +{ + return m_energyConsumedPhaseB; +} + +double SpeedwireMeter::energyConsumedPhaseC() const +{ + return m_energyConsumedPhaseC; +} + +double SpeedwireMeter::energyProducedPhaseA() const +{ + return m_energyProducedPhaseA; +} + +double SpeedwireMeter::energyProducedPhaseB() const +{ + return m_energyProducedPhaseB; +} + +double SpeedwireMeter::energyProducedPhaseC() const +{ + return m_energyProducedPhaseC; +} + +double SpeedwireMeter::currentPowerPhaseA() const +{ + return m_currentPowerPhaseA; +} + +double SpeedwireMeter::currentPowerPhaseB() const +{ + return m_currentPowerPhaseB; +} + +double SpeedwireMeter::currentPowerPhaseC() const +{ + return m_currentPowerPhaseC; +} + +double SpeedwireMeter::voltagePhaseA() const +{ + return m_voltagePhaseA; +} + +double SpeedwireMeter::voltagePhaseB() const +{ + return m_voltagePhaseB; +} + +double SpeedwireMeter::voltagePhaseC() const +{ + return m_voltagePhaseC; +} + +double SpeedwireMeter::amperePhaseA() const +{ + return m_amperePhaseA; +} + +double SpeedwireMeter::amperePhaseB() const +{ + return m_amperePhaseB; +} + +double SpeedwireMeter::amperePhaseC() const +{ + return m_amperePhaseC; +} + +QString SpeedwireMeter::softwareVersion() const +{ + return m_softwareVersion; +} + +void SpeedwireMeter::processData(const QByteArray &data) +{ + qCDebug(dcSma()) << "Meter: data received" << data.toHex(); + QDataStream stream(data); + stream.setByteOrder(QDataStream::BigEndian); + + SpeedwireInterface::SpeedwireHeader header = SpeedwireInterface::parseHeader(stream); + if (!header.isValid()) { + qCDebug(dcSma()) << "Meter: Datagram header is not valid. Ignoring data..."; + return; + } + + if (header.protocolId != SpeedwireInterface::ProtocolIdMeter) { + qCDebug(dcSma()) << "Meter: received header protocol which is not from the meter protocol. Ignoring data..."; + return; + } + + quint16 modelId; + quint32 serialNumber; + stream >> modelId >> serialNumber; + if (m_modelId != modelId && serialNumber != m_serialNumber) { + qCDebug(dcSma()) << "Meter: received meter data from an other meter. Ignoring data..."; + } + + qCDebug(dcSma()) << "Meter: Model ID:" << modelId; + qCDebug(dcSma()) << "Meter: Serial number:" << serialNumber; + + // Parse the packet data + // Timestamp e618a416 + qCDebug(dcSma()) << "Meter: ======================= Meter measurements"; + quint32 timestamp; + stream >> timestamp; + qCDebug(dcSma()) << "Meter: Timestamp:" << timestamp; + + // Obis data + //00 01 04 00 00000000 00 01 08 00 0000002139122910 00 02 04 00 00004415 00 02 08 00 0000001575a137d8 00 03 04 00 00000000 00 03 08 00 00000003debed0e8 00040400000017c6000408000000001008c2070000090400000000000009080000000027c77bed20000a04000000481d000a08000000001722823410000d0400000003b00015040000000000001508000000000d1e1e0e3000160400000015120016080000000006c5a2d8b800170400000000000017080000000001bd6f680000180400000007990018080000000004def712b8001d040000000000001d08000000000eeefaafd0001e040000001666001e0800000000074b38bf88001f040000000a300020040000037bcb00210400000003ad0029040000000000002908000000000a9b1afec8002a040000001a81002a08000000000803e62b88002b040000000000002b080000000001511459b8002c0400000006d5002c0800000000052c8455b80031040000000000003108000000000cf83b37100032040000001b5f0032080000000008a6e257f80033040000000c3f003404000003747900350400000003c8003d040000000000003d08000000000a53d0ba08003e040000001482003e080000000007800fd188003f040000000000003f080000000001185820c8004004000000095800400800000000064563b1900045040000000000004508000000000d26d3eae0004604000000168900460800000000082b4fc5a80047040000000a440048040000037ed1004904000000038e90000000 01020852 00000000 + while (!stream.atEnd()) { + quint8 measurementChannel; + quint8 measurementIndex; + quint8 measurmentType; + quint8 measurmentTariff; + + stream >> measurementChannel >> measurementIndex >> measurmentType >> measurmentTariff; + + if (measurmentType == 4) { + qint32 measurement; + stream >> measurement; + + if (measurementIndex == 1 && measurement != 0) { + m_currentPower = measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power" << m_currentPower << "W"; + } else if (measurementIndex == 2 && measurement != 0) { + m_currentPower = -measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power" << m_currentPower << "W"; + } else if (measurementIndex == 21 && measurement != 0) { + m_currentPowerPhaseA = measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power phase A" << m_currentPowerPhaseA << "W"; + } else if (measurementIndex == 22 && measurement != 0) { + m_currentPowerPhaseA = -measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power phase A" << m_currentPowerPhaseA << "W"; + } else if (measurementIndex == 41 && measurement != 0) { + m_currentPowerPhaseB = measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power phase B" << m_currentPowerPhaseB << "W"; + } else if (measurementIndex == 42 && measurement != 0) { + m_currentPowerPhaseB = -measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power phase B" << m_currentPowerPhaseB << "W"; + } else if (measurementIndex == 61 && measurement != 0) { + m_currentPowerPhaseC = measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power phase C" << m_currentPowerPhaseC << "W"; + } else if (measurementIndex == 62 && measurement != 0) { + m_currentPowerPhaseC = -measurement / 10.0; + qCDebug(dcSma()) << "Meter: Current power phase C" << m_currentPowerPhaseC << "W"; + } else if (measurementIndex == 31) { + m_amperePhaseA = measurement / 1000.0; + qCDebug(dcSma()) << "Meter: Ampere phase A" << m_amperePhaseA << "A"; + } else if (measurementIndex == 51) { + m_amperePhaseB = measurement / 1000.0; + qCDebug(dcSma()) << "Meter: Ampere phase B" << m_amperePhaseB << "A"; + } else if (measurementIndex == 71) { + m_amperePhaseC = measurement / 1000.0; + qCDebug(dcSma()) << "Meter: Ampere phase C" << m_amperePhaseC << "A"; + } else if (measurementIndex == 32) { + m_voltagePhaseA = measurement / 1000.0; + qCDebug(dcSma()) << "Meter: Voltage phase A" << m_voltagePhaseA << "V"; + } else if (measurementIndex == 52) { + m_voltagePhaseB = measurement / 1000.0; + qCDebug(dcSma()) << "Meter: Voltage phase B" << m_voltagePhaseB << "V"; + } else if (measurementIndex == 72) { + m_voltagePhaseC = measurement / 1000.0; + qCDebug(dcSma()) << "Meter: Voltage phase C" << m_voltagePhaseC << "V"; + } else { +// qCDebug(dcSma()) << "Meter: --> Channel:" << measurementChannel << "Index:" << measurementIndex << "Type:" << measurmentType << "Rate:" << measurmentTariff; +// qCDebug(dcSma()) << "Meter: Value:" << measurement; + } + + + } else if (measurmentType == 8) { + qint64 measurement; + stream >> measurement; + + if (measurementIndex == 1 && measurement != 0) { + m_totalEnergyConsumed = measurement / 3600000.0; + qCDebug(dcSma()) << "Total energy consumed" << m_totalEnergyConsumed << "kWh"; + } else if (measurementIndex == 2 && measurement != 0) { + m_totalEnergyProduced = measurement / 3600000.0; + qCDebug(dcSma()) << "Total energy produced" << m_totalEnergyProduced << "kWh"; + } else if (measurementIndex == 21 && measurement != 0) { + m_energyConsumedPhaseA = measurement / 3600000.0; + qCDebug(dcSma()) << "Energy consumed phase A" << m_energyConsumedPhaseA << "kWh"; + } else if (measurementIndex == 41 && measurement != 0) { + m_energyConsumedPhaseB = measurement / 3600000.0; + qCDebug(dcSma()) << "Energy consumed phase B" << m_energyConsumedPhaseB << "kWh"; + } else if (measurementIndex == 61 && measurement != 0) { + m_energyConsumedPhaseC = measurement / 3600000.0; + qCDebug(dcSma()) << "Energy consumed phase C" << m_energyConsumedPhaseC << "kWh"; + } else if (measurementIndex == 22 && measurement != 0) { + m_energyProducedPhaseA = measurement / 3600000.0; + qCDebug(dcSma()) << "Energy produced phase A" << m_energyProducedPhaseA << "kWh"; + } else if (measurementIndex == 42 && measurement != 0) { + m_energyProducedPhaseB = measurement / 3600000.0; + qCDebug(dcSma()) << "Energy produced phase B" << m_energyProducedPhaseB << "kWh"; + } else if (measurementIndex == 62 && measurement != 0) { + m_energyProducedPhaseC = measurement / 3600000.0; + qCDebug(dcSma()) << "Energy produced phase C" << m_energyProducedPhaseC << "kWh"; + } else { +// qCDebug(dcSma()) << "Meter: --> Channel:" << measurementChannel << "Index:" << measurementIndex << "Type:" << measurmentType << "Rate:" << measurmentTariff; +// qCDebug(dcSma()) << "Meter: Value:" << measurement; + } + + } if (measurementChannel == 144 && measurementIndex == 0 && measurmentType == 0 && measurmentTariff == 0) { + // Software version + // 90000000 01 02 08 52 + quint8 major, minor, build, revision; + stream >> major >> minor >> build >> revision; + // Revision types: + // S: Special version + // A: Alpha version + // B: Beta version + // R: Release version + // E: Experimental version + // N: No revision + m_softwareVersion = QString("%1.%2.%3-%4").arg(major).arg(minor).arg(build).arg(QChar(revision)); + + qCDebug(dcSma()) << "Meter: Software version" << m_softwareVersion; + } else if (measurementChannel == 0 && measurementIndex == 0 && measurmentType == 0 && measurmentTariff == 0) { + // 00 00 00 00 + //qCDebug(dcSma()) << "Meter: End of data reached."; + } + } + + emit valuesUpdated(); +} diff --git a/sma/speedwiremeter.h b/sma/speedwiremeter.h new file mode 100644 index 00000000..97e6e4f7 --- /dev/null +++ b/sma/speedwiremeter.h @@ -0,0 +1,86 @@ +#ifndef SPEEDWIREMETER_H +#define SPEEDWIREMETER_H + +#include + +#include "speedwireinterface.h" + +class SpeedwireMeter : public QObject +{ + Q_OBJECT +public: + explicit SpeedwireMeter(const QHostAddress &address, quint16 modelId, quint32 serialNumber, QObject *parent = nullptr); + + bool initialize(); + bool initialized() const; + + + double currentPower() const; + double totalEnergyProduced() const; + double totalEnergyConsumed() const; + + double energyConsumedPhaseA() const; + double energyConsumedPhaseB() const; + double energyConsumedPhaseC() const; + + double energyProducedPhaseA() const; + double energyProducedPhaseB() const; + double energyProducedPhaseC() const; + + double currentPowerPhaseA() const; + double currentPowerPhaseB() const; + double currentPowerPhaseC() const; + + double voltagePhaseA() const; + double voltagePhaseB() const; + double voltagePhaseC() const; + + double amperePhaseA() const; + double amperePhaseB() const; + double amperePhaseC() const; + + QString softwareVersion() const; + + +signals: + void valuesUpdated(); + +private: + SpeedwireInterface *m_interface = nullptr; + QHostAddress m_address; + bool m_initialized = false; + quint16 m_modelId = 0; + quint32 m_serialNumber = 0; + + double m_currentPower = 0; + double m_totalEnergyProduced = 0; + double m_totalEnergyConsumed = 0; + + double m_energyConsumedPhaseA = 0; + double m_energyConsumedPhaseB = 0; + double m_energyConsumedPhaseC = 0; + + double m_energyProducedPhaseA = 0; + double m_energyProducedPhaseB = 0; + double m_energyProducedPhaseC = 0; + + double m_currentPowerPhaseA = 0; + double m_currentPowerPhaseB = 0; + double m_currentPowerPhaseC = 0; + + double m_voltagePhaseA = 0; + double m_voltagePhaseB = 0; + double m_voltagePhaseC = 0; + + double m_amperePhaseA = 0; + double m_amperePhaseB = 0; + double m_amperePhaseC = 0; + + QString m_softwareVersion; + +private slots: + void processData(const QByteArray &data); + +}; + +#endif // SPEEDWIREMETER_H