mirror of
https://github.com/nymea/nymea-plugins.git
synced 2026-10-23 14:43:44 +02:00
Add V2 and V3 basic communication
This commit is contained in:
parent
1f0733db97
commit
37d9e1ae04
@ -8,4 +8,4 @@ Currently supported and tested models:
|
|||||||
|
|
||||||
## More
|
## More
|
||||||
|
|
||||||
https://senec.com
|
https://senec.com
|
||||||
|
|||||||
@ -36,6 +36,14 @@
|
|||||||
|
|
||||||
IntegrationPluginSenec::IntegrationPluginSenec()
|
IntegrationPluginSenec::IntegrationPluginSenec()
|
||||||
{
|
{
|
||||||
|
// Testing the convert methods
|
||||||
|
// QString rawValue = "fl_42A2E5E4"; // 81.449
|
||||||
|
// float value = SenecStorageLan::parseFloat(rawValue);
|
||||||
|
// qCWarning(dcSenec()) << rawValue << value;
|
||||||
|
|
||||||
|
// QString rawValue = "st_foobar";
|
||||||
|
// QString value = SenecStorageLan::parseString(rawValue);
|
||||||
|
// qCWarning(dcSenec()) << rawValue << value;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +52,52 @@ IntegrationPluginSenec::~IntegrationPluginSenec()
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void IntegrationPluginSenec::discoverThings(ThingDiscoveryInfo *info)
|
||||||
|
{
|
||||||
|
if (info->thingClassId() == senecConnectionThingClassId) {
|
||||||
|
|
||||||
|
if (!hardwareManager()->networkDeviceDiscovery()->available()) {
|
||||||
|
qCWarning(dcSenec()) << "Failed to discover network devices. The network device discovery is not available.";
|
||||||
|
info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("Unable to discover devices in your network."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// qCInfo(dcESPSomfyRTS()) << "Starting network discovery...";
|
||||||
|
// EspSomfyRtsDiscovery *discovery = new EspSomfyRtsDiscovery(hardwareManager()->networkManager(), hardwareManager()->networkDeviceDiscovery(), info);
|
||||||
|
// connect(discovery, &EspSomfyRtsDiscovery::discoveryFinished, info, [=](){
|
||||||
|
// ThingDescriptors descriptors;
|
||||||
|
// qCInfo(dcESPSomfyRTS()) << "Discovery finished. Found" << discovery->results().count() << "devices";
|
||||||
|
// foreach (const EspSomfyRtsDiscovery::Result &result, discovery->results()) {
|
||||||
|
// qCInfo(dcESPSomfyRTS()) << "Discovered device on" << result.networkDeviceInfo;
|
||||||
|
|
||||||
|
// QString title = "ESP Somfy RTS (" + result.name + ")";
|
||||||
|
// QString description = result.networkDeviceInfo.address().toString();
|
||||||
|
|
||||||
|
// ThingDescriptor descriptor(espSomfyRtsThingClassId, title, description);
|
||||||
|
|
||||||
|
// ParamList params;
|
||||||
|
// params << Param(espSomfyRtsThingMacAddressParamTypeId, result.networkDeviceInfo.thingParamValueMacAddress());
|
||||||
|
// params << Param(espSomfyRtsThingHostNameParamTypeId, result.networkDeviceInfo.thingParamValueHostName());
|
||||||
|
// params << Param(espSomfyRtsThingAddressParamTypeId, result.networkDeviceInfo.thingParamValueAddress());
|
||||||
|
// descriptor.setParams(params);
|
||||||
|
|
||||||
|
// // Check if we already have set up this device
|
||||||
|
// Thing *existingThing = myThings().findByParams(params);
|
||||||
|
// if (existingThing) {
|
||||||
|
// qCDebug(dcESPSomfyRTS()) << "This thing already exists in the system:" << result.networkDeviceInfo;
|
||||||
|
// descriptor.setThingId(existingThing->id());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// info->addThingDescriptor(descriptor);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// info->finish(Thing::ThingErrorNoError);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// discovery->startDiscovery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void IntegrationPluginSenec::startPairing(ThingPairingInfo *info)
|
void IntegrationPluginSenec::startPairing(ThingPairingInfo *info)
|
||||||
{
|
{
|
||||||
info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter username and password for your SENEC.home account."));
|
info->finish(Thing::ThingErrorNoError, QT_TR_NOOP("Please enter username and password for your SENEC.home account."));
|
||||||
@ -51,59 +105,62 @@ void IntegrationPluginSenec::startPairing(ThingPairingInfo *info)
|
|||||||
|
|
||||||
void IntegrationPluginSenec::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret)
|
void IntegrationPluginSenec::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret)
|
||||||
{
|
{
|
||||||
qCDebug(dcSenec()) << "Start logging in" << username << secret.left(2) + QString(secret.length() - 2, '*');
|
if (info->thingClassId() == senecAccountThingClassId) {
|
||||||
|
|
||||||
QVariantMap requestMap;
|
qCDebug(dcSenec()) << "Start logging in" << username << secret.left(2) + QString(secret.length() - 2, '*');
|
||||||
requestMap.insert("username", username);
|
|
||||||
requestMap.insert("password", secret);
|
|
||||||
|
|
||||||
QNetworkRequest request(SenecAccount::loginUrl());
|
QVariantMap requestMap;
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
requestMap.insert("username", username);
|
||||||
|
requestMap.insert("password", secret);
|
||||||
|
|
||||||
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(requestMap).toJson(QJsonDocument::Indented));
|
QNetworkRequest request(SenecAccount::loginUrl());
|
||||||
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||||
connect(reply, &QNetworkReply::finished, this, [reply, info, username, this] {
|
|
||||||
|
|
||||||
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(requestMap).toJson(QJsonDocument::Indented));
|
||||||
// Check HTTP status code
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
if (status != 200 || reply->error() != QNetworkReply::NoError) {
|
connect(reply, &QNetworkReply::finished, this, [reply, info, username, this] {
|
||||||
qCWarning(dcSenec()) << "Login request finished with error. Status:" << status << "Error:" << reply->errorString();
|
|
||||||
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Username or password is invalid."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: as of now (API 4.4.3) the login seems to return a static token, which does not require any refresh.
|
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
// Not as bad as saving user and password on the device, but almost ...
|
// Check HTTP status code
|
||||||
|
if (status != 200 || reply->error() != QNetworkReply::NoError) {
|
||||||
|
qCWarning(dcSenec()) << "Login request finished with error. Status:" << status << "Error:" << reply->errorString();
|
||||||
|
info->finish(Thing::ThingErrorAuthenticationFailure, QT_TR_NOOP("Username or password is invalid."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// https://documenter.getpostman.com/view/932140/2s9YXib2td
|
// Note: as of now (API 4.4.3) the login seems to return a static token, which does not require any refresh.
|
||||||
|
// Not as bad as saving user and password on the device, but almost ...
|
||||||
|
|
||||||
QByteArray responseData = reply->readAll();
|
// https://documenter.getpostman.com/view/932140/2s9YXib2td
|
||||||
|
|
||||||
QJsonParseError jsonError;
|
QByteArray responseData = reply->readAll();
|
||||||
QVariantMap responseMap = QJsonDocument::fromJson(responseData, &jsonError).toVariant().toMap();
|
|
||||||
if (jsonError.error != QJsonParseError::NoError) {
|
|
||||||
qCWarning(dcSenec()) << "Login request finished successfully, but the response contains invalid JSON object:" << responseData;
|
|
||||||
info->finish(Thing::ThingErrorAuthenticationFailure);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responseMap.contains("token") || !responseMap.contains("refreshToken")) {
|
QJsonParseError jsonError;
|
||||||
qCWarning(dcSenec()) << "Login request finished successfully, but the response JSON does not contain the expected properties" << qUtf8Printable(responseData);
|
QVariantMap responseMap = QJsonDocument::fromJson(responseData, &jsonError).toVariant().toMap();
|
||||||
info->finish(Thing::ThingErrorHardwareFailure);
|
if (jsonError.error != QJsonParseError::NoError) {
|
||||||
return;
|
qCWarning(dcSenec()) << "Login request finished successfully, but the response contains invalid JSON object:" << responseData;
|
||||||
}
|
info->finish(Thing::ThingErrorAuthenticationFailure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
QString token = responseMap.value("token").toString();
|
if (!responseMap.contains("token") || !responseMap.contains("refreshToken")) {
|
||||||
QString refreshToken = responseMap.value("refreshToken").toString();
|
qCWarning(dcSenec()) << "Login request finished successfully, but the response JSON does not contain the expected properties" << qUtf8Printable(responseData);
|
||||||
|
info->finish(Thing::ThingErrorHardwareFailure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
pluginStorage()->beginGroup(info->thingId().toString());
|
QString token = responseMap.value("token").toString();
|
||||||
pluginStorage()->setValue("username", username);
|
QString refreshToken = responseMap.value("refreshToken").toString();
|
||||||
pluginStorage()->setValue("token", token);
|
|
||||||
pluginStorage()->setValue("refreshToken", refreshToken);
|
|
||||||
pluginStorage()->endGroup();
|
|
||||||
|
|
||||||
info->finish(Thing::ThingErrorNoError);
|
pluginStorage()->beginGroup(info->thingId().toString());
|
||||||
});
|
pluginStorage()->setValue("username", username);
|
||||||
|
pluginStorage()->setValue("token", token);
|
||||||
|
pluginStorage()->setValue("refreshToken", refreshToken);
|
||||||
|
pluginStorage()->endGroup();
|
||||||
|
|
||||||
|
info->finish(Thing::ThingErrorNoError);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void IntegrationPluginSenec::setupThing(ThingSetupInfo *info)
|
void IntegrationPluginSenec::setupThing(ThingSetupInfo *info)
|
||||||
@ -357,7 +414,7 @@ void IntegrationPluginSenec::refresh(Thing *thing)
|
|||||||
// In that case we use the bigger power. Maybe we cloudl als sum them up, that should be tested...
|
// In that case we use the bigger power. Maybe we cloudl als sum them up, that should be tested...
|
||||||
|
|
||||||
float currentPower = 0;
|
float currentPower = 0;
|
||||||
if (batteryCharge != 0 && batteryCharge != 0) {
|
if (batteryCharge != 0 && batteryDischarge != 0) {
|
||||||
if (batteryCharge > batteryDischarge) {
|
if (batteryCharge > batteryDischarge) {
|
||||||
currentPower = batteryCharge;
|
currentPower = batteryCharge;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -34,9 +34,13 @@
|
|||||||
#include <plugintimer.h>
|
#include <plugintimer.h>
|
||||||
#include <integrations/integrationplugin.h>
|
#include <integrations/integrationplugin.h>
|
||||||
|
|
||||||
|
#include <network/networkaccessmanager.h>
|
||||||
|
#include <network/networkdevicemonitor.h>
|
||||||
|
|
||||||
#include "extern-plugininfo.h"
|
#include "extern-plugininfo.h"
|
||||||
|
|
||||||
#include "senecaccount.h"
|
#include "senecaccount.h"
|
||||||
|
#include "senecstoragelan.h"
|
||||||
|
|
||||||
class IntegrationPluginSenec : public IntegrationPlugin
|
class IntegrationPluginSenec : public IntegrationPlugin
|
||||||
{
|
{
|
||||||
@ -49,6 +53,8 @@ public:
|
|||||||
explicit IntegrationPluginSenec();
|
explicit IntegrationPluginSenec();
|
||||||
~IntegrationPluginSenec();
|
~IntegrationPluginSenec();
|
||||||
|
|
||||||
|
void discoverThings(ThingDiscoveryInfo *info) override;
|
||||||
|
|
||||||
void startPairing(ThingPairingInfo *info) override;
|
void startPairing(ThingPairingInfo *info) override;
|
||||||
void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override;
|
void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override;
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,50 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "ef9e9a4c-2f66-4194-adcc-23f401572399",
|
||||||
|
"name": "senecConnection",
|
||||||
|
"displayName": "SENEC Connection",
|
||||||
|
"interfaces": ["gateway", "networkdevice"],
|
||||||
|
"createMethods": ["Discovery", "User"],
|
||||||
|
"providedInterfaces": ["energystorage"],
|
||||||
|
"paramTypes": [
|
||||||
|
{
|
||||||
|
"id": "a45cabe9-9f76-405e-b47c-0a3d00bdd44b",
|
||||||
|
"name": "address",
|
||||||
|
"displayName": "Host address",
|
||||||
|
"type": "QString",
|
||||||
|
"inputType": "IPv4Address",
|
||||||
|
"defaultValue": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "51a1bbc0-28a5-43ac-9021-896acbf9413f",
|
||||||
|
"name": "hostName",
|
||||||
|
"displayName": "Host name",
|
||||||
|
"type": "QString",
|
||||||
|
"inputType": "TextLine",
|
||||||
|
"defaultValue": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0790e1b1-c8c9-488f-9cda-2a0bbf14b4af",
|
||||||
|
"name": "macAddress",
|
||||||
|
"displayName": "MAC address",
|
||||||
|
"type": "QString",
|
||||||
|
"inputType": "MacAddress",
|
||||||
|
"readOnly": true,
|
||||||
|
"defaultValue": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateTypes": [
|
||||||
|
{
|
||||||
|
"id": "a95292ad-08c0-405c-85f4-b47978584604",
|
||||||
|
"name": "connected",
|
||||||
|
"displayName": "Connected",
|
||||||
|
"type": "bool",
|
||||||
|
"defaultValue": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "senecStorage",
|
"name": "senecStorage",
|
||||||
"displayName": "SENEC.Home storage",
|
"displayName": "SENEC.Home storage",
|
||||||
@ -59,7 +103,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "0a16f7c8-2e55-4869-aeb3-574f22b40527",
|
"id": "0a16f7c8-2e55-4869-aeb3-574f22b40527",
|
||||||
"name":"addMeter",
|
"name": "addMeter",
|
||||||
"displayName": "Add meter",
|
"displayName": "Add meter",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
@ -68,7 +112,7 @@
|
|||||||
"paramTypes": [
|
"paramTypes": [
|
||||||
{
|
{
|
||||||
"id": "9a0fa7b1-bc35-44d4-b987-5ecb87fd8b00",
|
"id": "9a0fa7b1-bc35-44d4-b987-5ecb87fd8b00",
|
||||||
"name":"id",
|
"name": "id",
|
||||||
"displayName": "System ID",
|
"displayName": "System ID",
|
||||||
"type": "QString",
|
"type": "QString",
|
||||||
"readOnly": true,
|
"readOnly": true,
|
||||||
|
|||||||
@ -5,10 +5,12 @@ QT+= network
|
|||||||
SOURCES += \
|
SOURCES += \
|
||||||
integrationpluginsenec.cpp \
|
integrationpluginsenec.cpp \
|
||||||
senecaccount.cpp \
|
senecaccount.cpp \
|
||||||
seneclanstorage.cpp
|
senecdiscovery.cpp \
|
||||||
|
senecstoragelan.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
integrationpluginsenec.h \
|
integrationpluginsenec.h \
|
||||||
senecaccount.h \
|
senecaccount.h \
|
||||||
seneclanstorage.h
|
senecdiscovery.h \
|
||||||
|
senecstoragelan.h
|
||||||
|
|
||||||
|
|||||||
@ -58,11 +58,6 @@ QUrl SenecAccount::systemsUrl()
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SenecAccount::available() const
|
|
||||||
{
|
|
||||||
return m_available;
|
|
||||||
}
|
|
||||||
|
|
||||||
QNetworkReply *SenecAccount::getSystems()
|
QNetworkReply *SenecAccount::getSystems()
|
||||||
{
|
{
|
||||||
QNetworkRequest request(SenecAccount::systemsUrl());
|
QNetworkRequest request(SenecAccount::systemsUrl());
|
||||||
|
|||||||
@ -47,16 +47,11 @@ public:
|
|||||||
static QUrl loginUrl();
|
static QUrl loginUrl();
|
||||||
static QUrl systemsUrl();
|
static QUrl systemsUrl();
|
||||||
|
|
||||||
bool available() const;
|
|
||||||
|
|
||||||
QNetworkReply *getSystems();
|
QNetworkReply *getSystems();
|
||||||
QNetworkReply *getDashboard(const QString &id);
|
QNetworkReply *getDashboard(const QString &id);
|
||||||
QNetworkReply *getAbilities(const QString &id);
|
QNetworkReply *getAbilities(const QString &id);
|
||||||
QNetworkReply *getTechnicalData(const QString &id);
|
QNetworkReply *getTechnicalData(const QString &id);
|
||||||
|
|
||||||
signals:
|
|
||||||
bool availableChanged(bool available);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
NetworkAccessManager *m_networkManager = nullptr;
|
NetworkAccessManager *m_networkManager = nullptr;
|
||||||
|
|
||||||
@ -64,7 +59,6 @@ private:
|
|||||||
QString m_token;
|
QString m_token;
|
||||||
QString m_refreshToken;
|
QString m_refreshToken;
|
||||||
|
|
||||||
bool m_available = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // SENECACCOUNT_H
|
#endif // SENECACCOUNT_H
|
||||||
|
|||||||
103
senec/senecdiscovery.cpp
Normal file
103
senec/senecdiscovery.cpp
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
*
|
||||||
|
* Copyright 2013 - 2025, 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 "senecdiscovery.h"
|
||||||
|
#include "extern-plugininfo.h"
|
||||||
|
|
||||||
|
SenecDiscovery::SenecDiscovery(NetworkAccessManager *networkManager, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent)
|
||||||
|
: QObject{parent},
|
||||||
|
m_networkManager{networkManager},
|
||||||
|
m_networkDeviceDiscovery{networkDeviceDiscovery}
|
||||||
|
{
|
||||||
|
m_gracePeriodTimer.setSingleShot(true);
|
||||||
|
m_gracePeriodTimer.setInterval(3000);
|
||||||
|
connect(&m_gracePeriodTimer, &QTimer::timeout, this, [this](){
|
||||||
|
finishDiscovery();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SenecDiscovery::startDiscovery()
|
||||||
|
{
|
||||||
|
qCDebug(dcSenec()) << "Discovery: Searching SENEC energy storages in the network...";
|
||||||
|
m_startDateTime = QDateTime::currentDateTime();
|
||||||
|
|
||||||
|
NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover();
|
||||||
|
connect(discoveryReply, &NetworkDeviceDiscoveryReply::hostAddressDiscovered, this, &SenecDiscovery::checkNetworkDevice);
|
||||||
|
connect(discoveryReply, &NetworkDeviceDiscoveryReply::finished, this, [=](){
|
||||||
|
qCDebug(dcSenec()) << "Discovery: Network discovery finished. Found" << discoveryReply->networkDeviceInfos().count() << "network devices";
|
||||||
|
m_networkDeviceInfos = discoveryReply->networkDeviceInfos();
|
||||||
|
m_gracePeriodTimer.start();
|
||||||
|
discoveryReply->deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<SenecDiscovery::Result> SenecDiscovery::results() const
|
||||||
|
{
|
||||||
|
return m_results;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SenecDiscovery::checkNetworkDevice(const QHostAddress &address)
|
||||||
|
{
|
||||||
|
qCDebug(dcSenec()) << "Discovery: Verifying" << address;
|
||||||
|
|
||||||
|
SenecStorageLan *storage = new SenecStorageLan(m_networkManager, address, this);
|
||||||
|
m_storages.append(storage);
|
||||||
|
|
||||||
|
connect(storage, &SenecStorageLan::initializeFinished, this, [this, storage, address](bool success){
|
||||||
|
if (!success) {
|
||||||
|
cleanupStorage(storage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Successfully initialized
|
||||||
|
Result result;
|
||||||
|
result.deviceId = storage->deviceId();
|
||||||
|
result.address = address;
|
||||||
|
m_results.append(result);
|
||||||
|
qCInfo(dcSenec()) << "Found SENEC storage on" << address.toString() << storage->deviceId();
|
||||||
|
cleanupStorage(storage);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void SenecDiscovery::finishDiscovery()
|
||||||
|
{
|
||||||
|
qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch();
|
||||||
|
|
||||||
|
// Fill in all network device infos we have
|
||||||
|
for (int i = 0; i < m_results.count(); i++)
|
||||||
|
m_results[i].networkDeviceInfo = m_networkDeviceInfos.get(m_results.at(i).address);
|
||||||
|
|
||||||
|
qCDebug(dcSenec()) << "Discovery: Finished the discovery process. Found" << m_results.count()
|
||||||
|
<< "SENEC devices in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz");
|
||||||
|
m_gracePeriodTimer.stop();
|
||||||
|
|
||||||
|
emit discoveryFinished();
|
||||||
|
}
|
||||||
79
senec/senecdiscovery.h
Normal file
79
senec/senecdiscovery.h
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
*
|
||||||
|
* Copyright 2013 - 2025, 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 SENECDISCOVERY_H
|
||||||
|
#define SENECDISCOVERY_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include <network/networkaccessmanager.h>
|
||||||
|
#include <network/networkdevicediscovery.h>
|
||||||
|
|
||||||
|
#include "senecstoragelan.h"
|
||||||
|
|
||||||
|
class SenecDiscovery : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit SenecDiscovery(NetworkAccessManager *networkManager, NetworkDeviceDiscovery *networkDeviceDiscovery, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
typedef struct Result {
|
||||||
|
QString deviceId;
|
||||||
|
QHostAddress address;
|
||||||
|
NetworkDeviceInfo networkDeviceInfo;
|
||||||
|
} Result;
|
||||||
|
|
||||||
|
void startDiscovery();
|
||||||
|
|
||||||
|
QList<SenecDiscovery::Result> results() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void discoveryFinished();
|
||||||
|
|
||||||
|
private:
|
||||||
|
NetworkAccessManager *m_networkManager = nullptr;
|
||||||
|
NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr;
|
||||||
|
|
||||||
|
QTimer m_gracePeriodTimer;
|
||||||
|
QDateTime m_startDateTime;
|
||||||
|
|
||||||
|
NetworkDeviceInfos m_networkDeviceInfos;
|
||||||
|
QList<SenecStorageLan *> m_storages;
|
||||||
|
QList<SenecDiscovery::Result> m_results;
|
||||||
|
|
||||||
|
void checkNetworkDevice(const QHostAddress &address);
|
||||||
|
void cleanupStorage(SenecStorageLan *storage);
|
||||||
|
|
||||||
|
void finishDiscovery();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SENECDISCOVERY_H
|
||||||
@ -1,5 +0,0 @@
|
|||||||
#include "seneclanstorage.h"
|
|
||||||
|
|
||||||
SenecLanStorage::SenecLanStorage(QObject *parent)
|
|
||||||
: QObject{parent}
|
|
||||||
{}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
#ifndef SENECLANSTORAGE_H
|
|
||||||
#define SENECLANSTORAGE_H
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
class SenecLanStorage : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit SenecLanStorage(QObject *parent = nullptr);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // SENECLANSTORAGE_H
|
|
||||||
242
senec/senecstoragelan.cpp
Normal file
242
senec/senecstoragelan.cpp
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
*
|
||||||
|
* Copyright 2013 - 2025, 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 "senecstoragelan.h"
|
||||||
|
#include "extern-plugininfo.h"
|
||||||
|
|
||||||
|
#include <QDataStream>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonParseError>
|
||||||
|
|
||||||
|
SenecStorageLan::SenecStorageLan(NetworkAccessManager *networkManager, QObject *parent)
|
||||||
|
: QObject{parent},
|
||||||
|
m_networkManager{networkManager}
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SenecStorageLan::SenecStorageLan(NetworkAccessManager *networkManager, const QHostAddress &address, QObject *parent)
|
||||||
|
: QObject{parent},
|
||||||
|
m_networkManager{networkManager},
|
||||||
|
m_address{address}
|
||||||
|
{
|
||||||
|
updateUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
QHostAddress SenecStorageLan::address() const
|
||||||
|
{
|
||||||
|
return m_address;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SenecStorageLan::setAddress(const QHostAddress &address)
|
||||||
|
{
|
||||||
|
m_address = address;
|
||||||
|
updateUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl SenecStorageLan::url() const
|
||||||
|
{
|
||||||
|
return m_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SenecStorageLan::deviceId() const
|
||||||
|
{
|
||||||
|
return m_deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
float SenecStorageLan::capacity() const
|
||||||
|
{
|
||||||
|
return m_capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
float SenecStorageLan::maxChargePower() const
|
||||||
|
{
|
||||||
|
return m_maxChargePower;
|
||||||
|
}
|
||||||
|
|
||||||
|
float SenecStorageLan::maxDischargePower() const
|
||||||
|
{
|
||||||
|
return m_maxDischargePower;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float SenecStorageLan::parseFloat(const QString &value)
|
||||||
|
{
|
||||||
|
Q_ASSERT_X(value.left(3) == "fl_", "SenecStorageLan", "The given value does not seem to be a float, it is not starting with fl_");
|
||||||
|
QString hexString = value.right(value.length() - 3);
|
||||||
|
|
||||||
|
float result = 0;
|
||||||
|
quint32 dataValue = 0;
|
||||||
|
|
||||||
|
QByteArray rawData = QByteArray::fromHex(hexString.toUtf8());
|
||||||
|
Q_ASSERT_X(rawData.count() == 4, "ModbusDataUtils", "invalid raw data size for converting value to float32. The rawdate has not the expected size of 4.");
|
||||||
|
QDataStream stream(rawData);
|
||||||
|
stream >> dataValue;
|
||||||
|
memcpy(&result, &dataValue, sizeof(quint32));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SenecStorageLan::parseString(const QString &value)
|
||||||
|
{
|
||||||
|
Q_ASSERT_X(value.left(3) == "st_", "SenecStorageLan", "The given value does not seem to be a string, it is not starting with st_");
|
||||||
|
return value.right(value.length() - 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SenecStorageLan::initialize()
|
||||||
|
{
|
||||||
|
if (m_url.isValid()) {
|
||||||
|
qCWarning(dcSenec()) << "Cannot initialize the storage. The request URL is not valid. Maybe the IP is not known yet or invalid.";
|
||||||
|
emit initializeFinished(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the debug request is working
|
||||||
|
QVariantMap request;
|
||||||
|
request.insert("DEBUG", QVariantMap());
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_networkManager->post(QNetworkRequest(m_url), QJsonDocument::fromVariant(request).toJson());
|
||||||
|
connect(reply, &QNetworkReply::sslErrors, this, &SenecStorageLan::ignoreSslErrors);
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply] {
|
||||||
|
|
||||||
|
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
// Check HTTP status code
|
||||||
|
if (status != 200 || reply->error() != QNetworkReply::NoError) {
|
||||||
|
qCWarning(dcSenec()) << "Debug request finished with error. Status:" << status << "Error:" << reply->errorString();
|
||||||
|
setAvailable(false);
|
||||||
|
emit initializeFinished(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray responseData = reply->readAll();
|
||||||
|
|
||||||
|
QJsonParseError jsonError;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData, &jsonError);
|
||||||
|
QVariantMap responseMap = jsonDoc.toVariant().toMap();
|
||||||
|
if (jsonError.error != QJsonParseError::NoError) {
|
||||||
|
qCWarning(dcSenec()) << "Debug request finished successfully, but the response contains invalid JSON object:" << responseData;
|
||||||
|
setAvailable(false);
|
||||||
|
emit initializeFinished(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify content, if the content is valid, we assume we have found a SENEC storage
|
||||||
|
if (!responseMap.contains("DEBUG")) {
|
||||||
|
qCWarning(dcSenec()) << "Unexpected reponse data from debug request. Aborting...";
|
||||||
|
setAvailable(false);
|
||||||
|
emit initializeFinished(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(dcSenec()) << "Debug request finished successfully" << qUtf8Printable(jsonDoc.toJson());
|
||||||
|
|
||||||
|
// Request basic information
|
||||||
|
|
||||||
|
QVariantMap request;
|
||||||
|
QVariantMap factoryMap;
|
||||||
|
factoryMap.insert("DEVICE_ID", QString());
|
||||||
|
factoryMap.insert("DESIGN_CAPACITY", QString());
|
||||||
|
factoryMap.insert("MAX_CHARGE_POWER_DC", QString());
|
||||||
|
factoryMap.insert("MAX_DISCHARGE_POWER_DC", QString());
|
||||||
|
request.insert("FACTORY", factoryMap);
|
||||||
|
|
||||||
|
QNetworkReply *reply = m_networkManager->post(QNetworkRequest(m_url), QJsonDocument::fromVariant(request).toJson());
|
||||||
|
connect(reply, &QNetworkReply::sslErrors, this, &SenecStorageLan::ignoreSslErrors);
|
||||||
|
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||||
|
connect(reply, &QNetworkReply::finished, this, [this, reply] {
|
||||||
|
|
||||||
|
|
||||||
|
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
// Check HTTP status code
|
||||||
|
if (status != 200 || reply->error() != QNetworkReply::NoError) {
|
||||||
|
qCWarning(dcSenec()) << "Factory request finished with error. Status:" << status << "Error:" << reply->errorString();
|
||||||
|
setAvailable(false);
|
||||||
|
emit initializeFinished(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray responseData = reply->readAll();
|
||||||
|
|
||||||
|
QJsonParseError jsonError;
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData, &jsonError);
|
||||||
|
QVariantMap responseMap = jsonDoc.toVariant().toMap();
|
||||||
|
if (jsonError.error != QJsonParseError::NoError) {
|
||||||
|
qCWarning(dcSenec()) << "Factory request finished successfully, but the response contains invalid JSON object:" << responseData;
|
||||||
|
setAvailable(false);
|
||||||
|
emit initializeFinished(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCDebug(dcSenec()) << "Factory request finished successfully" << qUtf8Printable(jsonDoc.toJson());
|
||||||
|
|
||||||
|
QVariantMap factoryResponseMap = responseMap.value("FACTORY").toMap();
|
||||||
|
m_deviceId = parseString(factoryResponseMap.value("DEVICE_ID").toString());
|
||||||
|
m_capacity = parseFloat(factoryResponseMap.value("DESIGN_CAPACITY").toString());
|
||||||
|
m_maxChargePower = parseFloat(factoryResponseMap.value("MAX_CHARGE_POWER_DC").toString());
|
||||||
|
m_maxDischargePower = parseFloat(factoryResponseMap.value("MAX_DISCHARGE_POWER_DC").toString());
|
||||||
|
|
||||||
|
emit initializeFinished(true);
|
||||||
|
setAvailable(true);
|
||||||
|
|
||||||
|
qCDebug(dcSenec()) << "Initialized successfully";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SenecStorageLan::updateUrl()
|
||||||
|
{
|
||||||
|
QUrl url;
|
||||||
|
if (m_address.isNull()) {
|
||||||
|
m_url = url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
url.setScheme("https");
|
||||||
|
url.setHost(m_address.toString());
|
||||||
|
url.setPath("lala.cgi");
|
||||||
|
m_url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SenecStorageLan::setAvailable(bool available)
|
||||||
|
{
|
||||||
|
if (m_available == available)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_available = available;
|
||||||
|
emit availableChanged(m_available);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SenecStorageLan::ignoreSslErrors(const QList<QSslError> &errors)
|
||||||
|
{
|
||||||
|
qCDebug(dcSenec()) << "SSL errors occurred" << errors << "but we are ignoring them in the LAN environment...";
|
||||||
|
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
|
||||||
|
reply->ignoreSslErrors();
|
||||||
|
}
|
||||||
93
senec/senecstoragelan.h
Normal file
93
senec/senecstoragelan.h
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
*
|
||||||
|
* Copyright 2013 - 2025, 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 SENECSTORAGELAN_H
|
||||||
|
#define SENECSTORAGELAN_H
|
||||||
|
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QHostAddress>
|
||||||
|
|
||||||
|
#include <network/networkaccessmanager.h>
|
||||||
|
|
||||||
|
class SenecStorageLan : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit SenecStorageLan(NetworkAccessManager *networkManager, QObject *parent = nullptr);
|
||||||
|
explicit SenecStorageLan(NetworkAccessManager *networkManager, const QHostAddress &address, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QHostAddress address() const;
|
||||||
|
void setAddress(const QHostAddress &address);
|
||||||
|
|
||||||
|
QUrl url() const;
|
||||||
|
|
||||||
|
bool available() const;
|
||||||
|
|
||||||
|
QString deviceId() const;
|
||||||
|
float capacity() const;
|
||||||
|
float maxChargePower() const;
|
||||||
|
float maxDischargePower() const;
|
||||||
|
|
||||||
|
|
||||||
|
static float parseFloat(const QString &value);
|
||||||
|
static QString parseString(const QString &value);
|
||||||
|
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void initialize();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void initializeFinished(bool success);
|
||||||
|
void availableChanged(bool available);
|
||||||
|
|
||||||
|
private:
|
||||||
|
NetworkAccessManager *m_networkManager = nullptr;
|
||||||
|
|
||||||
|
QUrl m_url;
|
||||||
|
QHostAddress m_address;
|
||||||
|
|
||||||
|
bool m_available = false;
|
||||||
|
|
||||||
|
QString m_deviceId;
|
||||||
|
float m_capacity = 0;
|
||||||
|
float m_maxChargePower = 0;
|
||||||
|
float m_maxDischargePower = 0;
|
||||||
|
|
||||||
|
void updateUrl();
|
||||||
|
void setAvailable(bool available);
|
||||||
|
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void ignoreSslErrors(const QList<QSslError> &errors);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // SENECSTORAGELAN_H
|
||||||
Loading…
x
Reference in New Issue
Block a user