From f647b809c9a36f49eaf3a01752228ada6d693186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 14 Nov 2022 17:26:41 +0100 Subject: [PATCH] Add sunspec model --- fronius/fronius.pro | 4 +- fronius/integrationpluginfronius.cpp | 222 +++++++++++++++++++++++++- fronius/integrationpluginfronius.h | 6 + fronius/integrationpluginfronius.json | 185 +++++++++++++++++++++ 4 files changed, 415 insertions(+), 2 deletions(-) diff --git a/fronius/fronius.pro b/fronius/fronius.pro index e730ebf0..8361f1f6 100644 --- a/fronius/fronius.pro +++ b/fronius/fronius.pro @@ -1,6 +1,8 @@ include(../plugins.pri) -QT += network +PKGCONFIG += nymea-sunspec + +QT += network serialbus SOURCES += \ froniusnetworkreply.cpp \ diff --git a/fronius/integrationpluginfronius.cpp b/fronius/integrationpluginfronius.cpp index 378b04e3..a4accec5 100644 --- a/fronius/integrationpluginfronius.cpp +++ b/fronius/integrationpluginfronius.cpp @@ -202,6 +202,106 @@ void IntegrationPluginFronius::setupThing(ThingSetupInfo *info) info->finish(Thing::ThingErrorNoError); + } else if (thing->thingClassId() == sunspecStorageThingClassId) { + QHostAddress address(thing->paramValue(sunspecStorageThingAddressParamTypeId).toString()); + uint port(thing->paramValue(sunspecStorageThingPortParamTypeId).toUInt()); + + SunSpecConnection * connection = new SunSpecConnection(address, port, 1, SunSpecDataPoint::ByteOrderBigEndian, nullptr); + + connect(info, &ThingSetupInfo::aborted, connection, &SunSpecConnection::deleteLater); + + // Update all child things connected states for this connection + connect(connection, &SunSpecConnection::connectedChanged, thing, [this, connection, thing] (bool connected) { + if (connected) { + qCDebug(dcSunSpec()) << connection << "connected"; + } else { + qCWarning(dcSunSpec()) << connection << "disconnected"; + } + + thing->setStateValue("connected", connected); + }); + + // Only during setup + connect(connection, &SunSpecConnection::connectedChanged, info, [this, connection, info] (bool connected) { + //qCDebug(dcSunSpec()) << "SunSpec connected changed during setup:" << (connected ? "connected" : "disconnected"); + if (connected) { + connect(connection, &SunSpecConnection::discoveryFinished, info, [this, connection, info] (bool success) { + if (success) { + qCDebug(dcSunSpec()) << "Discovery finished successfully during setup of" << connection << ". Found SunSpec data on base register" << connection->baseRegister(); + m_sunspecConnections.insert(connection, info->thing()); + info->finish(Thing::ThingErrorNoError); + + Thing *thing = info->thing(); + + // Get the battery model + foreach (SunSpecModel *model, connection->models()) { + if (model->modelId() == SunSpecModelFactory::ModelIdStorage) { + SunSpecStorageModel *storage = qobject_cast(model); + m_sunSpecStorages.insert(thing, storage); + + connect(model, &SunSpecModel::blockUpdated, this, [storage, thing, model](){ + qCDebug(dcSunSpec()) << thing->name() << "block data updated" << storage; + + thing->setStateValue(sunspecStorageConnectedStateTypeId, true); + thing->setStateValue(sunspecStorageVersionStateTypeId, model->commonModelInfo().versionString); + + thing->setStateValue(sunspecStorageBatteryCriticalStateTypeId, storage->chaState() < 5); + thing->setStateValue(sunspecStorageBatteryLevelStateTypeId, qRound(storage->chaState())); + thing->setStateValue(sunspecStorageGridChargingStateTypeId, storage->chaGriSet() == SunSpecStorageModel::ChagrisetGrid); + thing->setStateValue(sunspecStorageEnableChargingStateTypeId, storage->storCtlMod().testFlag(SunSpecStorageModel::Storctl_modCharge)); + thing->setStateValue(sunspecStorageChargingRateStateTypeId, storage->wChaGra()); + thing->setStateValue(sunspecStorageDischargingRateStateTypeId, storage->wDisChaGra()); + + switch (storage->chaSt()) { + case SunSpecStorageModel::ChastOff: + thing->setStateValue(sunspecStorageBatteryLevelStateTypeId, "Off"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "idle"); + break; + case SunSpecStorageModel::ChastEmpty: + thing->setStateValue(sunspecStorageBatteryLevelStateTypeId, "Empty"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "idle"); + break; + case SunSpecStorageModel::ChastDischarging: + thing->setStateValue(sunspecStorageBatteryLevelStateTypeId, "Discharging"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "discharging"); + break; + case SunSpecStorageModel::ChastCharging: + thing->setStateValue(sunspecStorageBatteryLevelStateTypeId, "Charging"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "charging"); + break; + case SunSpecStorageModel::ChastFull: + thing->setStateValue(sunspecStorageBatteryLevelStateTypeId, "Full"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "idle"); + break; + case SunSpecStorageModel::ChastHolding: + thing->setStateValue(sunspecStorageBatteryLevelStateTypeId, "Holding"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "idle"); + break; + case SunSpecStorageModel::ChastTesting: + thing->setStateValue(sunspecStorageBatteryLevelStateTypeId, "Testing"); + thing->setStateValue(sunspecStorageChargingStateStateTypeId, "idle"); + break; + } + + }); + + } + } + + } else { + qCWarning(dcSunSpec()) << "Discovery finished with errors during setup of" << connection; + info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("The SunSpec discovery finished with errors. Please make sure this is a SunSpec device.")); + } + }); + // Perform initial discovery, finish if a valid base register has been found + connection->startDiscovery(); + } else { + info->finish(Thing::ThingErrorHardwareNotAvailable); + } + }); + + connection->connectDevice(); + } else { Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); } @@ -220,6 +320,11 @@ void IntegrationPluginFronius::postSetupThing(Thing *thing) foreach (FroniusSolarConnection *connection, m_froniusConnections.keys()) { refreshConnection(connection); } + + foreach (SunSpecStorageModel *model, m_sunSpecStorages.values()) { + model->readBlockData(); + + } }); m_connectionRefreshTimer->start(); @@ -250,7 +355,122 @@ void IntegrationPluginFronius::thingRemoved(Thing *thing) void IntegrationPluginFronius::executeAction(ThingActionInfo *info) { - Q_UNUSED(info) + Thing *thing = info->thing(); + Action action = info->action(); + + if (thing->thingClassId() == sunspecStorageThingClassId) { + SunSpecStorageModel *storage = qobject_cast(m_sunSpecStorages.value(thing)); + if (!storage) { + qWarning(dcSunSpec()) << "Could not find sunspec model instance for thing" << thing; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + + if (!thing->stateValue(sunspecStorageConnectedStateTypeId).toBool()) { + qWarning(dcSunSpec()) << "Could not execute action for" << thing << "because the SunSpec connection is not connected."; + info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The SunSpec connection is not connected.")); + return; + } + + if (action.actionTypeId() == sunspecStorageGridChargingActionTypeId) { + bool gridCharging = action.param(sunspecStorageGridChargingActionGridChargingParamTypeId).value().toBool(); + qCDebug(dcFronius()) << "Set grid charging" << (gridCharging ? SunSpecStorageModel::ChagrisetGrid : SunSpecStorageModel::ChagrisetPv); + QModbusReply *reply = storage->setChaGriSet(gridCharging ? SunSpecStorageModel::ChagrisetGrid : SunSpecStorageModel::ChagrisetPv); + if (!reply) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply]{ + if (reply->error() != QModbusDevice::NoError) { + qCDebug(dcFronius()) << "Set grid charging failed"; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + qCDebug(dcFronius()) << "Set grid charging finished successfully"; + info->finish(Thing::ThingErrorNoError); + }); + } else if (action.actionTypeId() == sunspecStorageEnableChargingActionTypeId || action.actionTypeId() == sunspecStorageEnableDischargingActionTypeId) { + SunSpecStorageModel::Storctl_modFlags controlModeFlags; + if (action.actionTypeId() == sunspecStorageEnableChargingActionTypeId) { + if (action.param(sunspecStorageEnableChargingActionEnableChargingParamTypeId).value().toBool()) { + controlModeFlags.setFlag(SunSpecStorageModel::Storctl_modCharge); + } + + if (thing->stateValue(sunspecStorageEnableDischargingStateTypeId).toBool()) + controlModeFlags.setFlag(SunSpecStorageModel::Storctl_modDiScharge); + + } + + if (action.actionTypeId() == sunspecStorageEnableDischargingActionTypeId) { + if (action.param(sunspecStorageEnableDischargingActionEnableDischargingParamTypeId).value().toBool()) { + controlModeFlags.setFlag(SunSpecStorageModel::Storctl_modDiScharge); + } + + if (thing->stateValue(sunspecStorageEnableChargingStateTypeId).toBool()) + controlModeFlags.setFlag(SunSpecStorageModel::Storctl_modCharge); + + } + + qCDebug(dcFronius()) << "Set charging control flag" << controlModeFlags; + + QModbusReply *reply = storage->setStorCtlMod(controlModeFlags); + if (!reply) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply]{ + if (reply->error() != QModbusDevice::NoError) { + info->finish(Thing::ThingErrorHardwareFailure); + qCDebug(dcFronius()) << "Set charging control flag finished with error"; + return; + } + if (info->action().actionTypeId() == sunspecStorageEnableChargingActionTypeId) { + info->thing()->setStateValue(sunspecStorageEnableChargingStateTypeId, info->action().param(sunspecStorageEnableChargingActionEnableChargingParamTypeId).value()); + } + + if (info->action().actionTypeId() == sunspecStorageEnableDischargingActionTypeId) { + info->thing()->setStateValue(sunspecStorageEnableDischargingStateTypeId, info->action().param(sunspecStorageEnableDischargingActionEnableDischargingParamTypeId).value()); + } + + info->finish(Thing::ThingErrorNoError); + qCDebug(dcFronius()) << "Set charging control flag finished successfully"; + }); + } else if (action.actionTypeId() == sunspecStorageChargingRateActionTypeId) { + QModbusReply *reply = storage->setInWRte(action.param(sunspecStorageChargingRateActionChargingRateParamTypeId).value().toInt()); + if (!reply) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply]{ + if (reply->error() != QModbusDevice::NoError) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + info->finish(Thing::ThingErrorNoError); + }); + } else if (action.actionTypeId() == sunspecStorageDischargingRateActionTypeId) { + QModbusReply *reply = storage->setOutWRte(action.param(sunspecStorageDischargingRateActionDischargingRateParamTypeId).value().toInt()); + if (!reply) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + connect(reply, &QModbusReply::finished, reply, &QModbusReply::deleteLater); + connect(reply, &QModbusReply::finished, info, [info, reply]{ + if (reply->error() != QModbusDevice::NoError) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + info->finish(Thing::ThingErrorNoError); + }); + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled action: %1").arg(action.actionTypeId().toString()).toUtf8()); + } + + } } void IntegrationPluginFronius::refreshConnection(FroniusSolarConnection *connection) diff --git a/fronius/integrationpluginfronius.h b/fronius/integrationpluginfronius.h index 8fec913b..2be6d0f8 100644 --- a/fronius/integrationpluginfronius.h +++ b/fronius/integrationpluginfronius.h @@ -34,6 +34,10 @@ #include "integrations/integrationplugin.h" #include "froniussolarconnection.h" +#include +#include +#include + #include #include #include @@ -60,6 +64,8 @@ private: PluginTimer *m_connectionRefreshTimer = nullptr; QHash m_froniusConnections; + QHash m_sunspecConnections; + QHash m_sunSpecStorages; void refreshConnection(FroniusSolarConnection *connection); diff --git a/fronius/integrationpluginfronius.json b/fronius/integrationpluginfronius.json index 4b745b0a..ce0a336e 100644 --- a/fronius/integrationpluginfronius.json +++ b/fronius/integrationpluginfronius.json @@ -302,6 +302,7 @@ "createMethods": ["auto"], "interfaces": [ "energystorage", "connectable"], "paramTypes": [ + { "id": "49087f31-abf5-4bb8-946b-a3626ee80566", "name": "id", @@ -387,6 +388,190 @@ "defaultValue": false } ] + }, + { + "name": "sunspecStorage", + "displayName": "Fronius SunSpec Storage", + "id": "5cdf938a-5680-4fdd-96cf-69a06b3b68ae", + "createMethods": [ "user" ], + "interfaces": ["connectable"], + "paramTypes": [ + { + "id": "115f1b9c-10f7-40e3-8dd9-7bd886e7d19c", + "name": "address", + "displayName": "Host address", + "type": "QString", + "inputType": "IPv4Address", + "defaultValue": "84.20.187.116" + }, + { + "id": "cefe2a6d-1312-4f3f-b1fa-839c1dc22418", + "name":"port", + "displayName": "Port", + "type": "int", + "defaultValue": 21502 + }, + { + "id": "a63cb6d6-29c7-4961-a341-f5b7289b1145", + "name":"modelId", + "displayName": "Model", + "type": "int", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "3f107844-00c5-4f39-86e5-485b3d1f5c1a", + "name":"modbusAddress", + "displayName": "Modbus address", + "type": "uint", + "readOnly": true, + "defaultValue": 0 + }, + { + "id": "1335ae67-7597-45d3-856f-2a56449b8a5f", + "name":"manufacturer", + "displayName": "Manufacturer", + "type": "QString", + "defaultValue": "Unkown", + "readOnly": true + }, + { + "id": "c860d44c-af8c-4e6c-b67e-481c73a1ad68", + "name":"deviceModel", + "displayName": "Device model", + "type": "QString", + "defaultValue": "Unkown", + "readOnly": true + }, + { + "id": "fb366e44-86f2-46ad-97d0-5b20d44eb6cb", + "name":"serialNumber", + "displayName": "Serial number", + "type": "QString", + "defaultValue": "Unkown", + "readOnly": true + } + ], + "stateTypes":[ + { + "id": "47755a1c-b4a7-4d50-b718-dd43ab6ee5cd", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "f57c9418-fd16-4d9d-b7c5-54c734754dd2", + "name": "batteryCritical", + "displayName": "Battery critical", + "displayNameEvent": "Battery critical changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "56930aec-93c7-4609-a0d1-0e24fc45e3da", + "name": "batteryLevel", + "displayName": "Battery level", + "displayNameEvent": "Battery level changed", + "type": "int", + "unit": "Percentage", + "minValue": 0, + "maxValue": 100, + "defaultValue": 0 + }, + { + "id": "1dd36a84-eacd-4bd7-adbd-3ab1ccfaa9ac", + "name": "storageStatus", + "displayName": "Status", + "displayNameEvent": "Status changed", + "type": "QString", + "possibleValues": [ + "Off", + "Empty", + "Discharging", + "Charging", + "Full", + "Holding", + "Testing" + ], + "defaultValue": "Off" + }, + { + "id": "c8f9cd0a-423a-40d0-94cc-8775cdf075fe", + "name": "chargingState", + "displayName": "Charging state", + "displayNameEvent": "Charging state changed", + "type": "QString", + "possibleValues": ["idle", "charging", "discharging"], + "defaultValue": "idle" + }, + { + "id": "12435c63-0a73-4b49-95e1-1d8c8f7e4578", + "name": "gridCharging", + "displayName": "Grid charging", + "displayNameEvent": "Grid charging changed", + "type": "bool", + "defaultValue": false, + "writable": true, + "displayNameAction": "Set grid charging" + }, + { + "id": "b80df52f-47aa-44cf-8199-54dc96eba2ad", + "name": "enableCharging", + "displayName": "Charging", + "displayNameEvent": "Charging changed", + "displayNameAction": "Enable charging", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "80924163-d2a7-409a-a5c2-e30dc2cd30a3", + "name": "enableDischarging", + "displayName": "Discharging", + "displayNameEvent": "Discharging changed", + "displayNameAction": "Enable discharging", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "a8954186-4264-4edf-873e-1507d45e40e4", + "name": "chargingRate", + "displayName": "Charging rate", + "displayNameEvent": "Charging rate changed", + "type": "int", + "minValue": 0, + "maxValue": 100, + "unit": "Percentage", + "defaultValue": false, + "writable": true, + "displayNameAction": "Set charging rate" + }, + { + "id": "67bc1367-453f-4f24-8942-978d9ffd43f5", + "name": "dischargingRate", + "displayName": "Discharging rate", + "displayNameEvent": "Discharging rate changed", + "type": "int", + "minValue": 0, + "maxValue": 100, + "unit": "Percentage", + "defaultValue": false, + "writable": true, + "displayNameAction": "Set discharging rate" + }, + { + "id": "6364e277-b168-4acf-b0f6-fe32ced05f0e", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "QString", + "defaultValue": "" + } + ] } ] }