Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions include/MqttAvailableHandler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#pragma once

#include "Arduino.h"
#include <functional>

typedef std::function<void()> MqttSendData;

class MqttAvailableHandler {
public:
explicit MqttAvailableHandler(MqttSendData sendDataFunction);

void send(const String &availableSubTopic, bool isAvailable);
void onMqttPublished(uint16_t messageId);
private:
MqttSendData _MqttSendData;

uint16_t _unavailableMessageId = 0;
};
4 changes: 2 additions & 2 deletions include/MqttHandleHass.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ class MqttHandleHassClass {
static void publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);

// Sensor
static void publishSensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishDtuSensor(const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishSensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category, const String& extra_availability_topic = "");
static void publishDtuSensor(const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category, const String& extra_availability_topic = "");
static void publishInverterSensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);

static void publishInverterField(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false);
Expand Down
5 changes: 5 additions & 0 deletions include/MqttHandleInverterTotal.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#pragma once

#include <TaskSchedulerDeclarations.h>
#include "MqttAvailableHandler.h"
#include <memory>

class MqttHandleInverterTotalClass {
public:
Expand All @@ -10,8 +12,11 @@ class MqttHandleInverterTotalClass {

private:
void loop();
bool isDataValid();
void sendData();

Task _loopTask;
std::unique_ptr<MqttAvailableHandler> _availableHandler;
};

extern MqttHandleInverterTotalClass MqttHandleInverterTotal;
12 changes: 10 additions & 2 deletions include/MqttSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,33 @@
#include <espMqttClient.h>
#include <mutex>

typedef std::function<void(uint16_t packetId)> MqttOnPublishCallback;
typedef uint16_t PacketId;

class MqttSettingsClass {
public:
MqttSettingsClass();
void init();
void performReconnect();
bool getConnected();
void publish(const String& subtopic, const String& payload);
void publishGeneric(const String& topic, const String& payload, const bool retain, const uint8_t qos = 0);
PacketId publish(const String& subtopic, const String& payload, const uint8_t qos = 0);
PacketId publishGeneric(const String& topic, const String& payload, const bool retain, const uint8_t qos = 0);

void subscribe(const String& topic, const uint8_t qos, const espMqttClientTypes::OnMessageCallback& cb);
void unsubscribe(const String& topic);

String getPrefix() const;
String getClientId() const;

void addOnPublishCallback(MqttOnPublishCallback callback);

private:
void NetworkEvent(network_event event);

void onMqttDisconnect(espMqttClientTypes::DisconnectReason reason);
void onMqttConnect(const bool sessionPresent);
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total);
void onMqttPublish(PacketId packetId);

void performConnect();
void performDisconnect();
Expand All @@ -38,6 +44,8 @@ class MqttSettingsClass {
Ticker _mqttReconnectTimer;
MqttSubscribeParser _mqttSubscribeParser;
std::mutex _clientLock;
MqttOnPublishCallback _onPacketCallback;

};

extern MqttSettingsClass MqttSettings;
3 changes: 3 additions & 0 deletions include/SunPosition.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class SunPositionClass {
bool sunsetTime(struct tm* info) const;
bool sunriseTime(struct tm* info) const;
void setDoRecalc(const bool doRecalc);
bool wasAroundSunset(unsigned long millis);

private:
void loop();
Expand All @@ -28,6 +29,8 @@ class SunPositionClass {
uint32_t _sunriseMinutes = 0;
uint32_t _sunsetMinutes = 0;

unsigned long _sunsetMillis = 0;

bool _isValidInfo = false;
std::atomic_bool _doRecalc = true;
uint32_t _lastSunPositionCalculatedYMD = 0;
Expand Down
2 changes: 1 addition & 1 deletion lib/Hoymiles/src/inverters/InverterAbstract.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ bool InverterAbstract::isProducing()

bool InverterAbstract::isReachable()
{
return _enablePolling && Statistics()->getRxFailureCount() <= _reachableThreshold;
return _enablePolling && Statistics()->getRxFailureCount() <= _reachableThreshold && Statistics()->getLastUpdate() > 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense and should have gone into its own PR 😉

}

void InverterAbstract::setEnablePolling(const bool enabled)
Expand Down
41 changes: 31 additions & 10 deletions src/Datastore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "Datastore.h"
#include "Configuration.h"
#include <Hoymiles.h>
#include <SunPosition.h>

DatastoreClass Datastore;

Expand All @@ -26,6 +27,8 @@ void DatastoreClass::loop()
return;
}

const bool isDayPeriod = SunPosition.isDayPeriod();

uint8_t isProducing = 0;
uint8_t isReachable = 0;
uint8_t pollEnabledCount = 0;
Expand Down Expand Up @@ -73,30 +76,30 @@ void DatastoreClass::loop()
}
}

if (inv->isReachable()) {
isReachable++;
} else {
if (inv->getEnablePolling()) {
_isAllEnabledReachable = false;
}
}

float inverterYieldTotal = 0;
for (auto& c : inv->Statistics()->getChannelsByType(TYPE_INV)) {
if (cfg->Poll_Enable) {
_totalAcYieldTotalEnabled += inv->Statistics()->getChannelFieldValue(TYPE_INV, c, FLD_YT);
inverterYieldTotal += inv->Statistics()->getChannelFieldValue(TYPE_INV, c, FLD_YT);
_totalAcYieldDayEnabled += inv->Statistics()->getChannelFieldValue(TYPE_INV, c, FLD_YD);

_totalAcYieldTotalDigits = max<unsigned int>(_totalAcYieldTotalDigits, inv->Statistics()->getChannelFieldDigits(TYPE_INV, c, FLD_YT));
_totalAcYieldDayDigits = max<unsigned int>(_totalAcYieldDayDigits, inv->Statistics()->getChannelFieldDigits(TYPE_INV, c, FLD_YD));
}
}
_totalAcYieldTotalEnabled += inverterYieldTotal;

float inverterAcPower = 0;
for (auto& c : inv->Statistics()->getChannelsByType(TYPE_AC)) {
if (cfg->Poll_Enable) {
inverterAcPower += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC);
}
if (inv->getEnablePolling()) {
_totalAcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC);
_totalAcPowerDigits = max<unsigned int>(_totalAcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_PAC));
}
}
if (inv->getEnablePolling()) {
_totalAcPowerEnabled += inverterAcPower;
}

for (auto& c : inv->Statistics()->getChannelsByType(TYPE_DC)) {
if (inv->getEnablePolling()) {
Expand All @@ -109,6 +112,24 @@ void DatastoreClass::loop()
}
}
}

if (inv->isReachable()) {
isReachable++;
} else {
if (inv->getEnablePolling()) {
_isAllEnabledReachable = false;
}
if(cfg->Poll_Enable && !isDayPeriod && !cfg->Poll_Enable_Night) {
// enabled, but we are not polling because we are at night
// such inverters still count as "working properly" as long as we have valid data from sunset
// at sunset, we expect low power, but nonzero YieldTotal

if(inv->Statistics()->getLastUpdate() == 0 || inverterYieldTotal < 1 || inverterAcPower > 1 || !SunPosition.wasAroundSunset(inv->Statistics()->getLastUpdate())) {
// no valid data -> unreachable
_isAllEnabledReachable = false;
}
}
}
}

_isAtLeastOneProducing = isProducing > 0;
Expand Down
26 changes: 26 additions & 0 deletions src/MqttAvailableHandler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#include "MqttAvailableHandler.h"
#include "MqttSettings.h"

MqttAvailableHandler::MqttAvailableHandler(MqttSendData sendDataFunction): _MqttSendData(sendDataFunction) {
using std::placeholders::_1;
MqttSettings.addOnPublishCallback(std::bind(&MqttAvailableHandler::onMqttPublished, this, _1));
}

void MqttAvailableHandler::send(const String &availableSubTopic, bool isAvailable) {
if(isAvailable) {
this->_unavailableMessageId = 0;
this->_MqttSendData();
MqttSettings.publish(availableSubTopic, String(isAvailable));
} else {
this->_unavailableMessageId = MqttSettings.publish(availableSubTopic, String(isAvailable), 1);
}
}

void MqttAvailableHandler::onMqttPublished(uint16_t messageId) {
if(this->_unavailableMessageId > 1 && messageId == this->_unavailableMessageId) {
// unavailable message was successfully published

// we can now publish our invalid data safely
this->_MqttSendData();
}
}
32 changes: 24 additions & 8 deletions src/MqttHandleHass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ void MqttHandleHassClass::publishConfig()
publishDtuSensor("Largest Free Heap Block", "dtu/heap/maxalloc", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Lifetime Minimum Free Heap", "dtu/heap/minfree", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);

publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE);
publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE);
publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE, "ac/is_valid");
publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE, "ac/is_valid");
publishDtuSensor("AC Power", "ac/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE);
publishDtuSensor("DC Power", "dc/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE);

Expand Down Expand Up @@ -166,6 +166,15 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
root["exp_aft"] = Hoymiles.getNumInverters() * max<uint32_t>(Hoymiles.PollInterval(), Configuration.get().Mqtt.PublishInterval) * inv->getReachableThreshold();
}

const CONFIG_T& config = Configuration.get();
root["avty_mode"] = "all";
root["avty"][0]["t"] = MqttSettings.getPrefix() + config.Mqtt.Lwt.Topic;
root["avty"][0]["pl_avail"] = config.Mqtt.Lwt.Value_Online;
root["avty"][0]["pl_not_avail"] = config.Mqtt.Lwt.Value_Offline;
root["avty"][1]["t"] = MqttSettings.getPrefix() + serial + "/" + "status/reachable";
root["avty"][1]["pl_avail"] = "1";
root["avty"][1]["pl_not_avail"] = "0";

publish(configTopic, root);
} else {
publish(configTopic, "");
Expand Down Expand Up @@ -378,7 +387,7 @@ void MqttHandleHassClass::publishSensor(
JsonDocument& doc,
const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic,
const String& unit_of_measure, const String& icon,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category, const String& extra_availability_topic)
{
String sensor_id = name;
sensor_id.toLowerCase();
Expand All @@ -391,9 +400,16 @@ void MqttHandleHassClass::publishSensor(
addCommonMetadata(doc, unit_of_measure, icon, device_class, state_class, category);

const CONFIG_T& config = Configuration.get();
doc["avty_t"] = MqttSettings.getPrefix() + config.Mqtt.Lwt.Topic;
doc["pl_avail"] = config.Mqtt.Lwt.Value_Online;
doc["pl_not_avail"] = config.Mqtt.Lwt.Value_Offline;
doc["avty_mode"] = "all";
doc["avty"][0]["t"] = MqttSettings.getPrefix() + config.Mqtt.Lwt.Topic;
doc["avty"][0]["pl_avail"] = config.Mqtt.Lwt.Value_Online;
doc["avty"][0]["pl_not_avail"] = config.Mqtt.Lwt.Value_Offline;
if (extra_availability_topic.length() > 0) {
doc["avty"][1]["t"] = MqttSettings.getPrefix() + extra_availability_topic;
doc["avty"][1]["pl_avail"] = "1";
doc["avty"][1]["pl_not_avail"] = "0";
}


const String configTopic = "sensor/" + root_device + "/" + sensor_id + "/config";
publish(configTopic, doc);
Expand All @@ -402,13 +418,13 @@ void MqttHandleHassClass::publishSensor(
void MqttHandleHassClass::publishDtuSensor(
const String& name, const String& state_topic,
const String& unit_of_measure, const String& icon,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category, const String& extra_availability_topic)
{
const String dtuId = getDtuUniqueId();

JsonDocument root;
createDtuInfo(root);
publishSensor(root, dtuId, dtuId, name, state_topic, unit_of_measure, icon, device_class, state_class, category);
publishSensor(root, dtuId, dtuId, name, state_topic, unit_of_measure, icon, device_class, state_class, category, extra_availability_topic);
}

void MqttHandleHassClass::publishInverterSensor(
Expand Down
24 changes: 17 additions & 7 deletions src/MqttHandleInverterTotal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ MqttHandleInverterTotalClass MqttHandleInverterTotal;
MqttHandleInverterTotalClass::MqttHandleInverterTotalClass()
: _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&MqttHandleInverterTotalClass::loop, this))
{
_availableHandler.reset(new MqttAvailableHandler(std::bind(&MqttHandleInverterTotalClass::sendData, this)));
}

void MqttHandleInverterTotalClass::init(Scheduler& scheduler)
Expand All @@ -22,6 +23,19 @@ void MqttHandleInverterTotalClass::init(Scheduler& scheduler)
_loopTask.enable();
}

bool MqttHandleInverterTotalClass::isDataValid() {
return Datastore.getIsAllEnabledReachable() && Datastore.getIsAtLeastOneReachable();
}

void MqttHandleInverterTotalClass::sendData() {
MqttSettings.publish("ac/power", String(Datastore.getTotalAcPowerEnabled(), Datastore.getTotalAcPowerDigits()));
MqttSettings.publish("ac/yieldtotal", String(Datastore.getTotalAcYieldTotalEnabled(), Datastore.getTotalAcYieldTotalDigits()));
MqttSettings.publish("ac/yieldday", String(Datastore.getTotalAcYieldDayEnabled(), Datastore.getTotalAcYieldDayDigits()));
MqttSettings.publish("dc/power", String(Datastore.getTotalDcPowerEnabled(), Datastore.getTotalDcPowerDigits()));
MqttSettings.publish("dc/irradiation", String(Datastore.getTotalDcIrradiation(), 3));
MqttSettings.publish("dc/is_valid", String(this->isDataValid()));
}

void MqttHandleInverterTotalClass::loop()
{
// Update interval from config
Expand All @@ -32,11 +46,7 @@ void MqttHandleInverterTotalClass::loop()
return;
}

MqttSettings.publish("ac/power", String(Datastore.getTotalAcPowerEnabled(), Datastore.getTotalAcPowerDigits()));
MqttSettings.publish("ac/yieldtotal", String(Datastore.getTotalAcYieldTotalEnabled(), Datastore.getTotalAcYieldTotalDigits()));
MqttSettings.publish("ac/yieldday", String(Datastore.getTotalAcYieldDayEnabled(), Datastore.getTotalAcYieldDayDigits()));
MqttSettings.publish("ac/is_valid", String(Datastore.getIsAllEnabledReachable()));
MqttSettings.publish("dc/power", String(Datastore.getTotalDcPowerEnabled(), Datastore.getTotalDcPowerDigits()));
MqttSettings.publish("dc/irradiation", String(Datastore.getTotalDcIrradiation(), 3));
MqttSettings.publish("dc/is_valid", String(Datastore.getIsAllEnabledReachable()));
// publishes is_valid and calls sendData when appropriate
_availableHandler->send("ac/is_valid", this->isDataValid());

}
Loading