diff --git a/senec/README.md b/senec/README.md index 78733ec0..b09cdf4c 100644 --- a/senec/README.md +++ b/senec/README.md @@ -8,4 +8,4 @@ Currently supported and tested models: ## More -https://senec.com \ No newline at end of file +https://senec.com diff --git a/senec/integrationpluginsenec.cpp b/senec/integrationpluginsenec.cpp index 7a4af96f..29d091de 100644 --- a/senec/integrationpluginsenec.cpp +++ b/senec/integrationpluginsenec.cpp @@ -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 { diff --git a/senec/integrationpluginsenec.h b/senec/integrationpluginsenec.h index 31440b0f..d69fea7d 100644 --- a/senec/integrationpluginsenec.h +++ b/senec/integrationpluginsenec.h @@ -34,9 +34,13 @@ #include #include +#include +#include + #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; diff --git a/senec/integrationpluginsenec.json b/senec/integrationpluginsenec.json index 65948091..e6156c06 100644 --- a/senec/integrationpluginsenec.json +++ b/senec/integrationpluginsenec.json @@ -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, diff --git a/senec/senec.pro b/senec/senec.pro index 42a9c622..5d1de98f 100644 --- a/senec/senec.pro +++ b/senec/senec.pro @@ -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 diff --git a/senec/senecaccount.cpp b/senec/senecaccount.cpp index b8aca2c8..a8dbfbb5 100644 --- a/senec/senecaccount.cpp +++ b/senec/senecaccount.cpp @@ -58,11 +58,6 @@ QUrl SenecAccount::systemsUrl() return url; } -bool SenecAccount::available() const -{ - return m_available; -} - QNetworkReply *SenecAccount::getSystems() { QNetworkRequest request(SenecAccount::systemsUrl()); diff --git a/senec/senecaccount.h b/senec/senecaccount.h index 32513195..b97fb116 100644 --- a/senec/senecaccount.h +++ b/senec/senecaccount.h @@ -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 diff --git a/senec/senecdiscovery.cpp b/senec/senecdiscovery.cpp new file mode 100644 index 00000000..511edf9b --- /dev/null +++ b/senec/senecdiscovery.cpp @@ -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 . +* +* 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::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(); +} diff --git a/senec/senecdiscovery.h b/senec/senecdiscovery.h new file mode 100644 index 00000000..6bfd99d6 --- /dev/null +++ b/senec/senecdiscovery.h @@ -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 . +* +* 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 +#include + +#include +#include + +#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 results() const; + +signals: + void discoveryFinished(); + +private: + NetworkAccessManager *m_networkManager = nullptr; + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + + QTimer m_gracePeriodTimer; + QDateTime m_startDateTime; + + NetworkDeviceInfos m_networkDeviceInfos; + QList m_storages; + QList m_results; + + void checkNetworkDevice(const QHostAddress &address); + void cleanupStorage(SenecStorageLan *storage); + + void finishDiscovery(); + +}; + +#endif // SENECDISCOVERY_H diff --git a/senec/seneclanstorage.cpp b/senec/seneclanstorage.cpp deleted file mode 100644 index 2f2c08eb..00000000 --- a/senec/seneclanstorage.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "seneclanstorage.h" - -SenecLanStorage::SenecLanStorage(QObject *parent) - : QObject{parent} -{} diff --git a/senec/seneclanstorage.h b/senec/seneclanstorage.h deleted file mode 100644 index 2f94fbdb..00000000 --- a/senec/seneclanstorage.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef SENECLANSTORAGE_H -#define SENECLANSTORAGE_H - -#include - -class SenecLanStorage : public QObject -{ - Q_OBJECT -public: - explicit SenecLanStorage(QObject *parent = nullptr); - -signals: -}; - -#endif // SENECLANSTORAGE_H diff --git a/senec/senecstoragelan.cpp b/senec/senecstoragelan.cpp new file mode 100644 index 00000000..916b5cd0 --- /dev/null +++ b/senec/senecstoragelan.cpp @@ -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 . +* +* 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 +#include +#include + +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 &errors) +{ + qCDebug(dcSenec()) << "SSL errors occurred" << errors << "but we are ignoring them in the LAN environment..."; + QNetworkReply *reply = qobject_cast(sender()); + reply->ignoreSslErrors(); +} diff --git a/senec/senecstoragelan.h b/senec/senecstoragelan.h new file mode 100644 index 00000000..9521f25f --- /dev/null +++ b/senec/senecstoragelan.h @@ -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 . +* +* 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 +#include +#include + +#include + +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 &errors); + +}; + +#endif // SENECSTORAGELAN_H