Add V2 and V3 basic communication

This commit is contained in:
Simon Stürz 2025-07-15 16:59:34 +02:00
parent 1f0733db97
commit 37d9e1ae04
13 changed files with 673 additions and 78 deletions

View File

@ -8,4 +8,4 @@ Currently supported and tested models:
## More
https://senec.com
https://senec.com

View File

@ -36,6 +36,14 @@
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)
{
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)
{
qCDebug(dcSenec()) << "Start logging in" << username << secret.left(2) + QString(secret.length() - 2, '*');
if (info->thingClassId() == senecAccountThingClassId) {
QVariantMap requestMap;
requestMap.insert("username", username);
requestMap.insert("password", secret);
qCDebug(dcSenec()) << "Start logging in" << username << secret.left(2) + QString(secret.length() - 2, '*');
QNetworkRequest request(SenecAccount::loginUrl());
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QVariantMap requestMap;
requestMap.insert("username", username);
requestMap.insert("password", secret);
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(requestMap).toJson(QJsonDocument::Indented));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [reply, info, username, this] {
QNetworkRequest request(SenecAccount::loginUrl());
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// 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;
}
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QJsonDocument::fromVariant(requestMap).toJson(QJsonDocument::Indented));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [reply, info, username, this] {
// 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 ...
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// 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;
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;
}
QByteArray responseData = reply->readAll();
if (!responseMap.contains("token") || !responseMap.contains("refreshToken")) {
qCWarning(dcSenec()) << "Login request finished successfully, but the response JSON does not contain the expected properties" << qUtf8Printable(responseData);
info->finish(Thing::ThingErrorHardwareFailure);
return;
}
QJsonParseError jsonError;
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;
}
QString token = responseMap.value("token").toString();
QString refreshToken = responseMap.value("refreshToken").toString();
if (!responseMap.contains("token") || !responseMap.contains("refreshToken")) {
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());
pluginStorage()->setValue("username", username);
pluginStorage()->setValue("token", token);
pluginStorage()->setValue("refreshToken", refreshToken);
pluginStorage()->endGroup();
QString token = responseMap.value("token").toString();
QString refreshToken = responseMap.value("refreshToken").toString();
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)
@ -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...
float currentPower = 0;
if (batteryCharge != 0 && batteryCharge != 0) {
if (batteryCharge != 0 && batteryDischarge != 0) {
if (batteryCharge > batteryDischarge) {
currentPower = batteryCharge;
} else {

View File

@ -34,9 +34,13 @@
#include <plugintimer.h>
#include <integrations/integrationplugin.h>
#include <network/networkaccessmanager.h>
#include <network/networkdevicemonitor.h>
#include "extern-plugininfo.h"
#include "senecaccount.h"
#include "senecstoragelan.h"
class IntegrationPluginSenec : public IntegrationPlugin
{
@ -49,6 +53,8 @@ public:
explicit IntegrationPluginSenec();
~IntegrationPluginSenec();
void discoverThings(ThingDiscoveryInfo *info) override;
void startPairing(ThingPairingInfo *info) override;
void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override;

View File

@ -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",
"displayName": "SENEC.Home storage",
@ -59,7 +103,7 @@
},
{
"id": "0a16f7c8-2e55-4869-aeb3-574f22b40527",
"name":"addMeter",
"name": "addMeter",
"displayName": "Add meter",
"type": "bool",
"defaultValue": false
@ -68,7 +112,7 @@
"paramTypes": [
{
"id": "9a0fa7b1-bc35-44d4-b987-5ecb87fd8b00",
"name":"id",
"name": "id",
"displayName": "System ID",
"type": "QString",
"readOnly": true,

View File

@ -5,10 +5,12 @@ QT+= network
SOURCES += \
integrationpluginsenec.cpp \
senecaccount.cpp \
seneclanstorage.cpp
senecdiscovery.cpp \
senecstoragelan.cpp
HEADERS += \
integrationpluginsenec.h \
senecaccount.h \
seneclanstorage.h
senecdiscovery.h \
senecstoragelan.h

View File

@ -58,11 +58,6 @@ QUrl SenecAccount::systemsUrl()
return url;
}
bool SenecAccount::available() const
{
return m_available;
}
QNetworkReply *SenecAccount::getSystems()
{
QNetworkRequest request(SenecAccount::systemsUrl());

View File

@ -47,16 +47,11 @@ public:
static QUrl loginUrl();
static QUrl systemsUrl();
bool available() const;
QNetworkReply *getSystems();
QNetworkReply *getDashboard(const QString &id);
QNetworkReply *getAbilities(const QString &id);
QNetworkReply *getTechnicalData(const QString &id);
signals:
bool availableChanged(bool available);
private:
NetworkAccessManager *m_networkManager = nullptr;
@ -64,7 +59,6 @@ private:
QString m_token;
QString m_refreshToken;
bool m_available = false;
};
#endif // SENECACCOUNT_H

103
senec/senecdiscovery.cpp Normal file
View 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
View 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

View File

@ -1,5 +0,0 @@
#include "seneclanstorage.h"
SenecLanStorage::SenecLanStorage(QObject *parent)
: QObject{parent}
{}

View File

@ -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
View 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
View 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