Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
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
2 changes: 2 additions & 0 deletions include/MqttHandleInverterTotal.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class MqttHandleInverterTotalClass {
void loop();

Task _loopTask;

unsigned int _connected_iterations = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Style: This should be CamelCase.

Copy link
Author

Choose a reason for hiding this comment

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

I have replaced this with an even better implementation. Instead of blindly waiting a few iterations, ac/is_valid is published with QOS 1, and the data is only published once the topic is confirmed to be published.

A less complex option would be to simply not publish data when ac/is_valid=false. However, that would be backwards incompatible for non-HA uses of mqtt.

};

extern MqttHandleInverterTotalClass MqttHandleInverterTotal;
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
2 changes: 1 addition & 1 deletion src/Datastore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ void DatastoreClass::loop()
if (inv->isReachable()) {
isReachable++;
} else {
if (inv->getEnablePolling()) {
if (cfg->Poll_Enable) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this necessary here but not elsewhere above and below?

Copy link
Author

Choose a reason for hiding this comment

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

It very well might, but this is the only one relevant for ac/is_valid, as that depends on isAllEnabledReachable().
The other things don't affect Home Assistant Energy Management. In my opinion, they should go into their own PR.

Copy link
Owner

Choose a reason for hiding this comment

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

The original logic was to set all available if they are suposed to be available. That means if you don't fetch inverter values at night, all other inverters are still valid because you intentional don't pull this specific inverter.
Example: You have two inverters, one at a solar panel, one at a battery (which can run the whole night). If you don't fetch the first one at night, the ac_total should be still valid because the second inverter works as expected (as so does the first one as it is suspected to be offline)

Copy link
Author

@functionpointer functionpointer Mar 11, 2025

Choose a reason for hiding this comment

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

I see, i hadn't considered that scenario.

ac/yieldtotal won't actually be valid after a reboot. The solar panel inverter's yieldtotal is assumed to be 0, causing two jumps in the ac/yieldtotal value: One jump down on reboot at night, and one jump back up in the morning.

To keep backwards compatibility, ac/is_valid behavior could be kept original, except at night we must additionally have nonzero data from all inverters with cfg->Poll_Enable && !cfg->Poll_Night_Enable and have at least one inverter with cfg->Poll_Enable & cfg->Poll_Night_Enable.
That would mean we stay available until reboot. Afterward, we will be unavailable until morning.

Copy link
Author

Choose a reason for hiding this comment

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

I have implemented that strategy now

_isAllEnabledReachable = false;
}
}
Expand Down
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
14 changes: 14 additions & 0 deletions src/MqttHandleInverterTotal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ MqttHandleInverterTotalClass::MqttHandleInverterTotalClass()

void MqttHandleInverterTotalClass::init(Scheduler& scheduler)
{
this->_connected_iterations = 0;
scheduler.addTask(_loopTask);
_loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND);
_loopTask.enable();
Expand All @@ -27,11 +28,24 @@ void MqttHandleInverterTotalClass::loop()
// Update interval from config
_loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND);

if (!MqttSettings.getConnected()) {
this->_connected_iterations = 0;
} else {
this->_connected_iterations++;
}

if (!MqttSettings.getConnected() || !Hoymiles.isAllRadioIdle()) {
_loopTask.forceNextIteration();
return;
}

if(this->_connected_iterations < 2) {
// publish is_valid false after connecting to ensure statistics don't get a wrong value during startup
MqttSettings.publish("ac/is_valid", String(false));
MqttSettings.publish("dc/is_valid", String(false));
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()));
Expand Down