Add update logic and basic lan storage mechanism

add-senec-integration
Simon Stürz 2025-07-17 13:46:45 +02:00
parent 37d9e1ae04
commit 9691f8c6c9
6 changed files with 310 additions and 40 deletions

View File

@ -29,22 +29,29 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationpluginsenec.h"
#include "senecdiscovery.h"
#include "plugininfo.h"
#include <QJsonDocument>
#include <QJsonParseError>
#include <network/networkdevicediscovery.h>
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 rawValue = "st_foobar"; // foobar
// QString value = SenecStorageLan::parseString(rawValue);
// qCWarning(dcSenec()) << rawValue << value;
// QString rawValue = "u8_64"; // 100
// quint8 value = SenecStorageLan::parseUInt8(rawValue);
// qCWarning(dcSenec()) << rawValue << value;
}
IntegrationPluginSenec::~IntegrationPluginSenec()
@ -54,7 +61,7 @@ IntegrationPluginSenec::~IntegrationPluginSenec()
void IntegrationPluginSenec::discoverThings(ThingDiscoveryInfo *info)
{
if (info->thingClassId() == senecConnectionThingClassId) {
if (info->thingClassId() == senecStorageLanThingClassId) {
if (!hardwareManager()->networkDeviceDiscovery()->available()) {
qCWarning(dcSenec()) << "Failed to discover network devices. The network device discovery is not available.";
@ -62,39 +69,39 @@ void IntegrationPluginSenec::discoverThings(ThingDiscoveryInfo *info)
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;
qCInfo(dcSenec()) << "Starting network discovery...";
SenecDiscovery *discovery = new SenecDiscovery(hardwareManager()->networkManager(), hardwareManager()->networkDeviceDiscovery(), info);
connect(discovery, &SenecDiscovery::discoveryFinished, info, [=](){
ThingDescriptors descriptors;
qCInfo(dcSenec()) << "Discovery finished. Found" << discovery->results().count() << "devices";
foreach (const SenecDiscovery::Result &result, discovery->results()) {
qCInfo(dcSenec()) << "Discovered device on" << result.networkDeviceInfo;
// QString title = "ESP Somfy RTS (" + result.name + ")";
// QString description = result.networkDeviceInfo.address().toString();
QString title = "SENEC connection (" + result.deviceId + ")";
QString description = result.networkDeviceInfo.address().toString();
// ThingDescriptor descriptor(espSomfyRtsThingClassId, title, description);
ThingDescriptor descriptor(senecStorageLanThingClassId, 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);
ParamList params;
params << Param(senecStorageLanThingMacAddressParamTypeId, result.networkDeviceInfo.thingParamValueMacAddress());
params << Param(senecStorageLanThingHostNameParamTypeId, result.networkDeviceInfo.thingParamValueHostName());
params << Param(senecStorageLanThingAddressParamTypeId, 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());
// }
// Check if we already have set up this device
Thing *existingThing = myThings().findByParams(params);
if (existingThing) {
qCDebug(dcSenec()) << "This thing already exists in the system:" << result.networkDeviceInfo;
descriptor.setThingId(existingThing->id());
}
// info->addThingDescriptor(descriptor);
// }
info->addThingDescriptor(descriptor);
}
// info->finish(Thing::ThingErrorNoError);
// });
info->finish(Thing::ThingErrorNoError);
});
// discovery->startDiscovery();
discovery->startDiscovery();
}
}
@ -182,6 +189,85 @@ void IntegrationPluginSenec::setupThing(ThingSetupInfo *info)
thing->setStateValue(senecAccountUserDisplayNameStateTypeId, username);
} if (thing->thingClassId() == senecStorageLanThingClassId) {
// Handle reconfigure
if (m_monitors.contains(thing)) {
qCDebug(dcSenec()) << "Unregister existing monitor and recreate a new one...";
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
}
NetworkDeviceMonitor *monitor = hardwareManager()->networkDeviceDiscovery()->registerMonitor(thing);
m_monitors.insert(thing, monitor);
SenecStorageLan *storage = new SenecStorageLan(hardwareManager()->networkManager(), this);
m_storages.insert(thing, storage);
storage->setAddress(monitor->networkDeviceInfo().address());
connect(monitor, &NetworkDeviceMonitor::reachableChanged, storage, [monitor, storage](bool reachable){
if (reachable) {
storage->setAddress(monitor->networkDeviceInfo().address());
}
});
connect(storage, &SenecStorageLan::availableChanged, thing, [thing](bool available){
thing->setStateValue(senecStorageLanConnectedStateTypeId, available);
// TODO: Update also child things
});
connect(storage, &SenecStorageLan::updatedFinished, thing, [storage, thing, this](bool success){
thing->setStateValue(senecStorageLanConnectedStateTypeId, storage->available());
// TODO: Update also child things
if (!success)
return;
thing->setStateValue(senecStorageLanCapacityStateTypeId, storage->capacity());
thing->setStateValue(senecStorageLanBatteryLevelStateTypeId, storage->batteryLevel());
thing->setStateValue(senecStorageLanBatteryCriticalStateTypeId, storage->batteryLevel() < 10.0);
thing->setStateValue(senecStorageLanCurrentPowerStateTypeId, storage->batteryPower());
// Check if we have a meter
Thing *meterThing = nullptr;
Things meterThings = myThings().filterByThingClassId(senecMeterThingClassId).filterByParentId(thing->id());
if (!meterThings.isEmpty())
meterThing = meterThings.first();
// If so, update
if (meterThing) {
meterThing->setStateValue(senecMeterCurrentPowerStateTypeId, storage->gridPower());
meterThing->setStateValue(senecMeterConnectedStateTypeId, true);
}
});
connect(thing, &Thing::settingChanged, this, [this, thing](const ParamTypeId &paramTypeId, const QVariant &value){
if (paramTypeId == senecStorageLanSettingsAddMeterParamTypeId) {
if (value.toBool()) {
// Check if we have to add the meter
if (myThings().filterByThingClassId(senecMeterThingClassId).filterByParentId(thing->id()).isEmpty()) {
qCDebug(dcSenec()) << "Add meter for" << thing->name();
emit autoThingsAppeared(ThingDescriptors() << ThingDescriptor(senecMeterThingClassId, "SENEC Meter", QString(), thing->id()));
}
} else {
// Check if we have to remove the meter
Things existingMeters = myThings().filterByThingClassId(senecMeterThingClassId).filterByParentId(thing->id());
if (!existingMeters.isEmpty()) {
qCDebug(dcSenec()) << "Remove meter thing for" << thing->name();
emit autoThingDisappeared(existingMeters.takeFirst()->id());
}
}
}
});
info->finish(Thing::ThingErrorNoError);
} else if (thing->thingClassId() == senecStorageThingClassId) {
connect(thing, &Thing::settingChanged, this, [this, thing](const ParamTypeId &paramTypeId, const QVariant &value){
@ -325,6 +411,7 @@ void IntegrationPluginSenec::postSetupThing(Thing *thing)
connect(m_refreshTimer, &PluginTimer::timeout, this, [this](){
refresh();
});
m_refreshTimer->start();
}
}
@ -341,6 +428,11 @@ void IntegrationPluginSenec::thingRemoved(Thing *thing)
pluginStorage()->endGroup();
}
if (thing->thingClassId() == senecStorageLanThingClassId) {
hardwareManager()->networkDeviceDiscovery()->unregisterMonitor(m_monitors.take(thing));
m_storages.take(thing)->deleteLater();
}
if (myThings().isEmpty()) {
qCDebug(dcSenec()) << "Stopping refresh timer";
hardwareManager()->pluginTimerManager()->unregisterTimer(m_refreshTimer);
@ -361,6 +453,10 @@ void IntegrationPluginSenec::refresh(Thing *thing)
refresh(storageThing);
}
foreach (Thing *storageLanThing, myThings().filterByThingClassId(senecStorageLanThingClassId)) {
m_storages.value(storageLanThing)->update();
}
return;
}

View File

@ -66,7 +66,10 @@ public:
private:
PluginTimer *m_refreshTimer = nullptr;
QHash<Thing *, SenecAccount *> m_accounts;
QHash<Thing *, NetworkDeviceMonitor *> m_monitors;
QHash<Thing *, SenecStorageLan *> m_storages;
private slots:
void refresh(Thing *thing = nullptr);

View File

@ -41,12 +41,20 @@
]
},
{
"id": "ef9e9a4c-2f66-4194-adcc-23f401572399",
"name": "senecConnection",
"displayName": "SENEC Connection",
"interfaces": ["gateway", "networkdevice"],
"id": "345df3b1-d411-4db5-bbb2-3b14eb86c1ba",
"name": "senecStorageLan",
"displayName": "SENEC.Home storage",
"interfaces": ["energystorage", "networkdevice"],
"createMethods": ["Discovery", "User"],
"providedInterfaces": ["energystorage"],
"settingsTypes": [
{
"id": "239d388c-f4a2-4055-88c3-decc6dace2b8",
"name": "addMeter",
"displayName": "Add meter",
"type": "bool",
"defaultValue": false
}
],
"paramTypes": [
{
"id": "a45cabe9-9f76-405e-b47c-0a3d00bdd44b",
@ -76,11 +84,54 @@
],
"stateTypes": [
{
"id": "a95292ad-08c0-405c-85f4-b47978584604",
"id": "d3bdc64b-67fb-4d84-97ed-c5142e5db55d",
"name": "connected",
"displayName": "Connected",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "3b00470d-11bc-4352-a885-52ab9443cee5",
"name": "currentPower",
"displayName": "Current power",
"type": "double",
"unit": "Watt",
"defaultValue": 0.00,
"cached": true
},
{
"id": "66a3a7d2-1210-4891-a8f2-6fe4f9b8c6cc",
"name": "batteryLevel",
"displayName": "Battery level",
"type": "int",
"unit": "Percentage",
"minValue": 0,
"maxValue": 100,
"defaultValue": 0
},
{
"id": "a9aa13fb-3f51-40e3-9d29-ab882513a87a",
"name": "batteryCritical",
"displayName": "Battery critical",
"type": "bool",
"defaultValue": false
},
{
"id": "339f602f-c9f1-446d-bc96-1c7f9a7db922",
"name": "chargingState",
"displayName": "Charging state",
"type": "QString",
"possibleValues": ["idle", "charging", "discharging"],
"defaultValue": "idle"
},
{
"id": "d927e6cf-29f0-412f-b74d-7d803fd3e191",
"name": "capacity",
"displayName": "Capacity",
"type": "double",
"unit": "KiloWattHour",
"defaultValue": 0
}
]
},

View File

@ -45,7 +45,7 @@ SenecDiscovery::SenecDiscovery(NetworkAccessManager *networkManager, NetworkDevi
void SenecDiscovery::startDiscovery()
{
qCDebug(dcSenec()) << "Discovery: Searching SENEC energy storages in the network...";
qCInfo(dcSenec()) << "Discovery: Searching SENEC energy storages in the local network...";
m_startDateTime = QDateTime::currentDateTime();
NetworkDeviceDiscoveryReply *discoveryReply = m_networkDeviceDiscovery->discover();
@ -72,6 +72,7 @@ void SenecDiscovery::checkNetworkDevice(const QHostAddress &address)
connect(storage, &SenecStorageLan::initializeFinished, this, [this, storage, address](bool success){
if (!success) {
qCDebug(dcSenec()) << "Discovery: Failed to initialize host" << address.toString() << "Continue ...";
cleanupStorage(storage);
return;
}
@ -81,10 +82,18 @@ void SenecDiscovery::checkNetworkDevice(const QHostAddress &address)
result.deviceId = storage->deviceId();
result.address = address;
m_results.append(result);
qCInfo(dcSenec()) << "Found SENEC storage on" << address.toString() << storage->deviceId();
qCInfo(dcSenec()) << "Discovery: Found SENEC storage on" << address.toString() << storage->deviceId();
cleanupStorage(storage);
});
storage->initialize();
}
void SenecDiscovery::cleanupStorage(SenecStorageLan *storage)
{
m_storages.removeAll(storage);
storage->deleteLater();
}
void SenecDiscovery::finishDiscovery()
@ -95,7 +104,7 @@ void SenecDiscovery::finishDiscovery()
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()
qCInfo(dcSenec()) << "Discovery: Finished the discovery process. Found" << m_results.count()
<< "SENEC devices in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz");
m_gracePeriodTimer.stop();

View File

@ -66,6 +66,11 @@ QUrl SenecStorageLan::url() const
return m_url;
}
bool SenecStorageLan::available() const
{
return m_available;
}
QString SenecStorageLan::deviceId() const
{
return m_deviceId;
@ -86,6 +91,25 @@ float SenecStorageLan::maxDischargePower() const
return m_maxDischargePower;
}
float SenecStorageLan::batteryLevel() const
{
return m_batteryLevel;
}
float SenecStorageLan::batteryPower() const
{
return m_batteryPower;
}
float SenecStorageLan::gridPower() const
{
return m_gridPower;
}
float SenecStorageLan::inverterPower() const
{
return m_inverterPower;
}
float SenecStorageLan::parseFloat(const QString &value)
{
@ -109,6 +133,12 @@ QString SenecStorageLan::parseString(const QString &value)
return value.right(value.length() - 3);
}
// quint8 SenecStorageLan::parseUInt8(const QString &value)
// {
// Q_ASSERT_X(value.left(3) == "u8_", "SenecStorageLan", "The given value does not seem to be a uint8, it is not starting with u8_");
// }
void SenecStorageLan::initialize()
{
@ -211,6 +241,70 @@ void SenecStorageLan::initialize()
});
}
void SenecStorageLan::update()
{
if (m_url.isValid()) {
qCDebug(dcSenec()) << "Cannot update the storage. The request URL is not valid. Maybe the IP is not known yet or invalid.";
emit updatedFinished(false);
return;
}
QVariantMap request;
QVariantMap energyMap;
energyMap.insert("GUI_BAT_DATA_POWER", QString());
energyMap.insert("GUI_INVERTER_POWER", QString());
energyMap.insert("GUI_BAT_DATA_FUEL_CHARGE", QString());
energyMap.insert("GUI_HOUSE_POW", QString());
energyMap.insert("GUI_GRID_POW", QString());
request.insert("ENERGY", energyMap);
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()) << "Update request finished with error. Status:" << status << "Error:" << reply->errorString();
setAvailable(false);
emit updatedFinished(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()) << "Update request finished successfully, but the response contains invalid JSON object:" << responseData;
setAvailable(false);
emit updatedFinished(false);
return;
}
qCDebug(dcSenec()) << "Update request finished successfully" << qUtf8Printable(jsonDoc.toJson());
QVariantMap energyResponseMap = responseMap.value("ENERGY").toMap();
m_batteryPower = parseFloat(energyResponseMap.value("GUI_BAT_DATA_POWER").toString());
m_batteryLevel = parseFloat(energyResponseMap.value("GUI_BAT_DATA_FUEL_CHARGE").toString());
m_gridPower = parseFloat(energyResponseMap.value("GUI_GRID_POW").toString());
m_inverterPower = parseFloat(energyResponseMap.value("GUI_INVERTER_POWER").toString());
setAvailable(true);
qCDebug(dcSenec()).nospace().noquote() << "Update values: Battery power: " << m_batteryPower
<< "W, Battery level: " << m_batteryLevel
<< "%, Grid power: " << m_gridPower
<< "W, Inverter power: " << m_inverterPower << "W";
emit updatedFinished(true);
});
}
void SenecStorageLan::updateUrl()
{
QUrl url;

View File

@ -37,10 +37,13 @@
#include <network/networkaccessmanager.h>
// https://blog.odenthal.cc/query-the-hidden-api-of-your-senec-photovoltaik-appliance/
class SenecStorageLan : public QObject
{
Q_OBJECT
public:
explicit SenecStorageLan(NetworkAccessManager *networkManager, QObject *parent = nullptr);
explicit SenecStorageLan(NetworkAccessManager *networkManager, const QHostAddress &address, QObject *parent = nullptr);
@ -51,22 +54,32 @@ public:
bool available() const;
// Init properties
QString deviceId() const;
float capacity() const;
float maxChargePower() const;
float maxDischargePower() const;
// Update properties
float batteryLevel() const;
float batteryPower() const;
float gridPower() const;
float inverterPower() const;
static float parseFloat(const QString &value);
static QString parseString(const QString &value);
static float parseFloat(const QString &value); // fl_
static QString parseString(const QString &value); // st_
//static quint8 parseUInt8(const QString &value); // u8_
public slots:
void initialize();
void update();
signals:
void initializeFinished(bool success);
void availableChanged(bool available);
void updatedFinished(bool success);
private:
NetworkAccessManager *m_networkManager = nullptr;
@ -81,10 +94,14 @@ private:
float m_maxChargePower = 0;
float m_maxDischargePower = 0;
float m_batteryLevel = 0;
float m_batteryPower = 0;
float m_gridPower = 0;
float m_inverterPower = 0;
void updateUrl();
void setAvailable(bool available);
private slots:
void ignoreSslErrors(const QList<QSslError> &errors);