monitor: Event logs for nonfunc fan sensors
This commit adds the code to create event logs calling out the fan when
fan sensors have been nonfunctional for a certain amount of time.
This functionality is configured in the JSON, and will only be enabled
if the 'fault_handling' JSON section is present. It uses the following
new JSON parameters:
nonfunc_rotor_error_delay (per fan):
This says how many seconds a fan sensor must be nonfunctional before the
event log will be created.
num_nonfunc_rotors_before_error (under fault_handling):
This specifies how many nonfunctional fan rotors there must be at the
same time before an event log with an error severity is created for the
rotor. When there are fewer than this many nonfunctional rotors, then
event logs with an informational severity will be created.
A new FanError class is used to create the event logs. It adds the
Logger output as FFDC, plus any JSON data that is passed in with the
commit() API. It uses CALLOUT_INVENTORY_PATH in the AdditionalData
property to specify the faulted fan FRU.
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I365114357580b4f38ec943a769c1ce7f695b51ab
diff --git a/monitor/Makefile.am b/monitor/Makefile.am
index 2c7a7fa..78b0b57 100644
--- a/monitor/Makefile.am
+++ b/monitor/Makefile.am
@@ -7,6 +7,7 @@
phosphor_fan_monitor_SOURCES = \
argument.cpp \
fan.cpp \
+ fan_error.cpp \
power_interface.cpp \
logging.cpp \
main.cpp \
diff --git a/monitor/fan.cpp b/monitor/fan.cpp
index b5092c0..114b625 100644
--- a/monitor/fan.cpp
+++ b/monitor/fan.cpp
@@ -70,7 +70,8 @@
mode, bus, *this, std::get<sensorNameField>(s),
std::get<hasTargetField>(s), std::get<funcDelay>(def),
std::get<targetInterfaceField>(s), std::get<factorField>(s),
- std::get<offsetField>(s), std::get<timeoutField>(def), event));
+ std::get<offsetField>(s), std::get<timeoutField>(def),
+ std::get<nonfuncRotorErrDelayField>(def), event));
_trustManager->registerSensor(_sensors.back());
}
@@ -287,6 +288,15 @@
_system.fanStatusChange(*this);
}
}
+
+void Fan::sensorErrorTimerExpired(const TachSensor& sensor)
+{
+ if (_present)
+ {
+ _system.sensorErrorTimerExpired(*this, sensor);
+ }
+}
+
} // namespace monitor
} // namespace fan
} // namespace phosphor
diff --git a/monitor/fan.hpp b/monitor/fan.hpp
index a7db524..187d626 100644
--- a/monitor/fan.hpp
+++ b/monitor/fan.hpp
@@ -158,6 +158,14 @@
return _present;
}
+ /**
+ * @brief Called from TachSensor when its error timer expires
+ * so an event log calling out the fan can be created.
+ *
+ * @param[in] sensor - The nonfunctional sensor
+ */
+ void sensorErrorTimerExpired(const TachSensor& sensor);
+
private:
/**
* @brief Returns true if the sensor input is not within
diff --git a/monitor/fan_error.cpp b/monitor/fan_error.cpp
new file mode 100644
index 0000000..067858b
--- /dev/null
+++ b/monitor/fan_error.cpp
@@ -0,0 +1,148 @@
+/**
+ * Copyright © 2020 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "fan_error.hpp"
+
+#include "logging.hpp"
+#include "sdbusplus.hpp"
+
+#include <nlohmann/json.hpp>
+#include <xyz/openbmc_project/Logging/Create/server.hpp>
+
+#include <filesystem>
+
+namespace phosphor::fan::monitor
+{
+
+using FFDCFormat =
+ sdbusplus::xyz::openbmc_project::Logging::server::Create::FFDCFormat;
+using FFDCFiles = std::vector<
+ std::tuple<FFDCFormat, uint8_t, uint8_t, sdbusplus::message::unix_fd>>;
+using json = nlohmann::json;
+
+const auto loggingService = "xyz.openbmc_project.Logging";
+const auto loggingPath = "/xyz/openbmc_project/logging";
+const auto loggingCreateIface = "xyz.openbmc_project.Logging.Create";
+
+namespace fs = std::filesystem;
+using namespace phosphor::fan::util;
+
+FFDCFile::FFDCFile(const fs::path& name) :
+ _fd(open(name.c_str(), O_RDONLY)), _name(name)
+{
+ if (_fd() == -1)
+ {
+ auto e = errno;
+ getLogger().log(fmt::format("Could not open FFDC file {}. errno {}",
+ _name.string(), e));
+ }
+}
+
+void FanError::commit(const json& jsonFFDC)
+{
+ FFDCFiles ffdc;
+ auto ad = getAdditionalData();
+
+ // Add the Logger contents as FFDC
+ auto logFile = makeLogFFDCFile();
+ if (logFile && (logFile->fd() != -1))
+ {
+ ffdc.emplace_back(FFDCFormat::Text, 0x01, 0x01, logFile->fd());
+ }
+
+ // Add the passed in JSON as FFDC
+ auto ffdcFile = makeJsonFFDCFile(jsonFFDC);
+ if (ffdcFile && (ffdcFile->fd() != -1))
+ {
+ ffdc.emplace_back(FFDCFormat::JSON, 0x01, 0x01, ffdcFile->fd());
+ }
+
+ try
+ {
+ SDBusPlus::callMethod(loggingService, loggingPath, loggingCreateIface,
+ "CreateWithFFDCFiles", _errorName, _severity, ad,
+ ffdc);
+ }
+ catch (const DBusError& e)
+ {
+ getLogger().log(
+ fmt::format("Call to create a {} error for fan {} failed: {}",
+ _errorName, _fanName, e.what()),
+ Logger::error);
+ }
+}
+
+std::map<std::string, std::string> FanError::getAdditionalData()
+{
+ std::map<std::string, std::string> ad;
+
+ ad.emplace("_PID", std::to_string(getpid()));
+ ad.emplace("CALLOUT_INVENTORY_PATH", _fanName);
+
+ if (!_sensorName.empty())
+ {
+ ad.emplace("FAN_SENSOR", _sensorName);
+ }
+
+ return ad;
+}
+
+std::unique_ptr<FFDCFile> FanError::makeLogFFDCFile()
+{
+ try
+ {
+ auto logFile = getLogger().saveToTempFile();
+ return std::make_unique<FFDCFile>(logFile);
+ }
+ catch (const std::exception& e)
+ {
+ log<level::ERR>(
+ fmt::format("Could not save log contents in FFDC. Error msg: {}",
+ e.what())
+ .c_str());
+ }
+ return nullptr;
+}
+
+std::unique_ptr<FFDCFile> FanError::makeJsonFFDCFile(const json& ffdcData)
+{
+ char tmpFile[] = "/tmp/fanffdc.XXXXXX";
+ auto fd = mkstemp(tmpFile);
+ if (fd != -1)
+ {
+ auto jsonString = ffdcData.dump();
+
+ auto rc = write(fd, jsonString.data(), jsonString.size());
+ close(fd);
+ if (rc != -1)
+ {
+ fs::path jsonFile{tmpFile};
+ return std::make_unique<FFDCFile>(jsonFile);
+ }
+ else
+ {
+ getLogger().log("Failed call to write JSON FFDC file");
+ }
+ }
+ else
+ {
+ auto e = errno;
+ getLogger().log(fmt::format("Failed called to mkstemp, errno = {}", e),
+ Logger::error);
+ }
+ return nullptr;
+}
+
+} // namespace phosphor::fan::monitor
diff --git a/monitor/fan_error.hpp b/monitor/fan_error.hpp
new file mode 100644
index 0000000..f190758
--- /dev/null
+++ b/monitor/fan_error.hpp
@@ -0,0 +1,166 @@
+#pragma once
+
+#include "utility.hpp"
+
+#include <nlohmann/json.hpp>
+#include <xyz/openbmc_project/Logging/Entry/server.hpp>
+
+#include <filesystem>
+#include <string>
+#include <tuple>
+
+namespace phosphor::fan::monitor
+{
+
+/**
+ * @class FFDCFile
+ *
+ * This class holds a file that is used for event log FFDC
+ * which needs a file descriptor as input. The file is
+ * deleted upon destruction.
+ */
+class FFDCFile
+{
+ public:
+ FFDCFile() = delete;
+ FFDCFile(const FFDCFile&) = delete;
+ FFDCFile& operator=(const FFDCFile&) = delete;
+ FFDCFile(FFDCFile&&) = delete;
+ FFDCFile& operator=(FFDCFile&&) = delete;
+
+ /**
+ * @brief Constructor
+ *
+ * Opens the file and saves the descriptor
+ *
+ * @param[in] name - The filename
+ */
+ explicit FFDCFile(const std::filesystem::path& name);
+
+ /**
+ * @brief Destructor - Deletes the file
+ */
+ ~FFDCFile()
+ {
+ std::filesystem::remove(_name);
+ }
+
+ /**
+ * @brief Returns the file descriptor
+ *
+ * @return int - The descriptor
+ */
+ int fd()
+ {
+ return _fd();
+ }
+
+ private:
+ /**
+ * @brief The file descriptor holder
+ */
+ util::FileDescriptor _fd;
+
+ /**
+ * @brief The filename
+ */
+ const std::filesystem::path _name;
+};
+
+/**
+ * @class FanError
+ *
+ * This class represents a fan error. It has a commit() interface
+ * that will create the event log with certain FFDC.
+ */
+class FanError
+{
+ public:
+ FanError() = delete;
+ ~FanError() = default;
+ FanError(const FanError&) = delete;
+ FanError& operator=(const FanError&) = delete;
+ FanError(FanError&&) = delete;
+ FanError& operator=(FanError&&) = delete;
+
+ /**
+ * @brief Constructor
+ *
+ * @param[in] error - The error name, like
+ * xyz.openbmc_project.Fan.Error.Fault
+ * @param[in] fan - The failing fan's inventory path
+ * @param[in] sensor - The failing sensor's inventory path. Can be empty
+ * if the error is for the FRU and not the sensor.
+ * @param[in] severity - The severity of the error
+ */
+ FanError(const std::string& error, const std::string& fan,
+ const std::string& sensor,
+ sdbusplus::xyz::openbmc_project::Logging::server::Entry::Level
+ severity) :
+ _errorName(error),
+ _fanName(fan), _sensorName(sensor),
+ _severity(
+ sdbusplus::xyz::openbmc_project::Logging::server::convertForMessage(
+ severity))
+ {}
+
+ /**
+ * @brief Commits the error by calling the D-Bus method to create
+ * the event log.
+ *
+ * The FFDC is passed in here so that if an error is committed
+ * more than once it can have up to date FFDC.
+ *
+ * @param[in] jsonFFDC - Free form JSON data that should be sent in as
+ * FFDC.
+ */
+ void commit(const nlohmann::json& jsonFFDC);
+
+ private:
+ /**
+ * @brief Returns an FFDCFile holding the Logger contents
+ *
+ * @return std::unique_ptr<FFDCFile> - The file object
+ */
+ std::unique_ptr<FFDCFile> makeLogFFDCFile();
+
+ /**
+ * @brief Returns an FFDCFile holding the contents of the JSON FFDC
+ *
+ * @param[in] ffdcData - The JSON data to write to a file
+ *
+ * @return std::unique_ptr<FFDCFile> - The file object
+ */
+ std::unique_ptr<FFDCFile> makeJsonFFDCFile(const nlohmann::json& ffdcData);
+
+ /**
+ * @brief Create and returns the AdditionalData property to use for the
+ * event log.
+ *
+ * @return map<string, string> - The AdditionalData contents
+ */
+ std::map<std::string, std::string> getAdditionalData();
+
+ /**
+ * @brief The error name (The event log's 'Message' property)
+ */
+ const std::string _errorName;
+
+ /**
+ * @brief The inventory name of the failing fan
+ */
+ const std::string _fanName;
+
+ /**
+ * @brief The inventory name of the failing sensor, if there is one.
+ */
+ const std::string _sensorName;
+
+ /**
+ * @brief The severity of the event log. This is the string
+ * representation of the Entry::Level property.
+ */
+ const std::string _severity;
+};
+
+} // namespace phosphor::fan::monitor
diff --git a/monitor/gen-fan-monitor-defs.py b/monitor/gen-fan-monitor-defs.py
index 43b0bb9..310f475 100755
--- a/monitor/gen-fan-monitor-defs.py
+++ b/monitor/gen-fan-monitor-defs.py
@@ -55,6 +55,7 @@
${fan_data['deviation']},
${fan_data['num_sensors_nonfunc_for_fan_nonfunc']},
0, // Monitor start delay - not used in YAML configs
+ std::nullopt, // nonfuncRotorErrorDelay - also not used here
std::vector<SensorDefinition>{
%for sensor in fan_data['sensors']:
<%
diff --git a/monitor/json_parser.cpp b/monitor/json_parser.cpp
index 1c06f01..46ae049 100644
--- a/monitor/json_parser.cpp
+++ b/monitor/json_parser.cpp
@@ -210,6 +210,19 @@
fan["num_sensors_nonfunc_for_fan_nonfunc"].get<size_t>();
}
+ // nonfunc_rotor_error_delay is optional, though it will
+ // default to zero if 'fault_handling' is present.
+ std::optional<size_t> nonfuncRotorErrorDelay;
+ if (fan.contains("nonfunc_rotor_error_delay"))
+ {
+ nonfuncRotorErrorDelay =
+ fan["nonfunc_rotor_error_delay"].get<size_t>();
+ }
+ else if (obj.contains("fault_handling"))
+ {
+ nonfuncRotorErrorDelay = 0;
+ }
+
// Handle optional conditions
auto cond = std::optional<Condition>();
if (fan.contains("condition"))
@@ -244,7 +257,7 @@
std::tuple(fan["inventory"].get<std::string>(), funcDelay,
fan["allowed_out_of_range_time"].get<size_t>(),
fan["deviation"].get<size_t>(), nonfuncSensorsCount,
- monitorDelay, sensorDefs, cond));
+ monitorDelay, nonfuncRotorErrorDelay, sensorDefs, cond));
}
return fanDefs;
@@ -398,4 +411,18 @@
return rules;
}
+std::optional<size_t> getNumNonfuncRotorsBeforeError(const json& obj)
+{
+ std::optional<size_t> num;
+
+ if (obj.contains("fault_handling"))
+ {
+ // Defaults to 1 if not present inside of 'fault_handling'.
+ num = obj.at("fault_handling")
+ .value("num_nonfunc_rotors_before_error", 1);
+ }
+
+ return num;
+}
+
} // namespace phosphor::fan::monitor
diff --git a/monitor/json_parser.hpp b/monitor/json_parser.hpp
index 84876ff..ba53fda 100644
--- a/monitor/json_parser.hpp
+++ b/monitor/json_parser.hpp
@@ -93,4 +93,13 @@
getPowerOffRules(const json& obj,
std::shared_ptr<PowerInterfaceBase>& powerInterface);
+/**
+ * @brief Returns the 'num_nonfunc_rotors_before_error field
+ *
+ * @param[in] obj - JSON object to parse from
+ *
+ * @return optional<size_t> - The value, or std::nullopt if not present
+ */
+std::optional<size_t> getNumNonfuncRotorsBeforeError(const json& obj);
+
} // namespace phosphor::fan::monitor
diff --git a/monitor/system.cpp b/monitor/system.cpp
index f9a8804..c8bacca 100644
--- a/monitor/system.cpp
+++ b/monitor/system.cpp
@@ -24,6 +24,8 @@
#include "json_parser.hpp"
#endif
+#include "fan_error.hpp"
+
#include <nlohmann/json.hpp>
#include <phosphor-logging/log.hpp>
#include <sdbusplus/bus.hpp>
@@ -34,6 +36,8 @@
{
using json = nlohmann::json;
+using Severity = sdbusplus::xyz::openbmc_project::Logging::server::Entry::Level;
+
using namespace phosphor::logging;
System::System(Mode mode, sdbusplus::bus::bus& bus,
@@ -187,6 +191,8 @@
std::make_shared<PowerInterface>();
_powerOffRules = getPowerOffRules(jsonObj, powerInterface);
+
+ _numNonfuncSensorsBeforeError = getNumNonfuncRotorsBeforeError(jsonObj);
#endif
}
@@ -211,4 +217,71 @@
}
}
+void System::sensorErrorTimerExpired(const Fan& fan, const TachSensor& sensor)
+{
+ std::string fanPath{util::INVENTORY_PATH + fan.getName()};
+
+ getLogger().log(
+ fmt::format("Creating event log for faulted fan {} sensor {}", fanPath,
+ sensor.name()),
+ Logger::error);
+
+ // In order to know if the event log should have a severity of error or
+ // informational, count the number of existing nonfunctional sensors and
+ // compare it to _numNonfuncSensorsBeforeError.
+ size_t nonfuncSensors = 0;
+ for (const auto& fan : _fans)
+ {
+ for (const auto& s : fan->sensors())
+ {
+ // Don't count nonfunctional sensors that still have their
+ // error timer running as nonfunctional since they haven't
+ // had event logs created for those errors yet.
+ if (!s->functional() && !s->errorTimerRunning())
+ {
+ nonfuncSensors++;
+ }
+ }
+ }
+
+ Severity severity = Severity::Error;
+ if (nonfuncSensors < _numNonfuncSensorsBeforeError)
+ {
+ severity = Severity::Informational;
+ }
+
+ auto error =
+ std::make_unique<FanError>("xyz.openbmc_project.Fan.Error.Fault",
+ fanPath, sensor.name(), severity);
+
+ auto sensorData = captureSensorData();
+ error->commit(sensorData);
+
+ // TODO: save error so it can be committed again on a power off
+}
+
+json System::captureSensorData()
+{
+ json data;
+
+ for (const auto& fan : _fans)
+ {
+ for (const auto& sensor : fan->sensors())
+ {
+ json values;
+ values["present"] = fan->present();
+ values["functional"] = sensor->functional();
+ values["tach"] = sensor->getInput();
+ if (sensor->hasTarget())
+ {
+ values["target"] = sensor->getTarget();
+ }
+
+ data["sensors"][sensor->name()] = values;
+ }
+ }
+
+ return data;
+}
+
} // namespace phosphor::fan::monitor
diff --git a/monitor/system.hpp b/monitor/system.hpp
index b1b80a5..05e08b5 100644
--- a/monitor/system.hpp
+++ b/monitor/system.hpp
@@ -73,6 +73,16 @@
*/
void fanStatusChange(const Fan& fan);
+ /**
+ * @brief Called when a fan sensor's error timer expires, which
+ * happens when the sensor has been nonfunctional for a
+ * certain amount of time. An event log will be created.
+ *
+ * @param[in] fan - The parent fan of the sensor
+ * @param[in] sensor - The faulted sensor
+ */
+ void sensorErrorTimerExpired(const Fan& fan, const TachSensor& sensor);
+
private:
/* The mode of fan monitor */
Mode _mode;
@@ -106,6 +116,22 @@
std::vector<std::unique_ptr<PowerOffRule>> _powerOffRules;
/**
+ * @brief The number of concurrently nonfunctional fan sensors
+ * there must be for an event log created due to a
+ * nonfunctional fan sensor to have an Error severity as
+ * opposed to an Informational one.
+ */
+ std::optional<size_t> _numNonfuncSensorsBeforeError;
+
+ /**
+ * @brief Captures tach sensor data as JSON for use in
+ * fan fault and fan missing event logs.
+ *
+ * @return json - The JSON data
+ */
+ json captureSensorData();
+
+ /**
* @brief Retrieve the configured trust groups
*
* @param[in] jsonObj - JSON object to parse from
diff --git a/monitor/tach_sensor.cpp b/monitor/tach_sensor.cpp
index 5e85ba7..637ff68 100644
--- a/monitor/tach_sensor.cpp
+++ b/monitor/tach_sensor.cpp
@@ -71,13 +71,15 @@
const std::string& id, bool hasTarget, size_t funcDelay,
const std::string& interface, double factor,
int64_t offset, size_t timeout,
+ const std::optional<size_t>& errorDelay,
const sdeventplus::Event& event) :
_bus(bus),
_fan(fan), _name(FAN_SENSOR_PATH + id), _invName(path(fan.getName()) / id),
_hasTarget(hasTarget), _funcDelay(funcDelay), _interface(interface),
_factor(factor), _offset(offset), _timeout(timeout),
_timerMode(TimerMode::func),
- _timer(event, std::bind(&Fan::timerExpired, &fan, std::ref(*this)))
+ _timer(event, std::bind(&Fan::timerExpired, &fan, std::ref(*this))),
+ _errorDelay(errorDelay)
{
// Start from a known state of functional
setFunctional(true);
@@ -124,6 +126,14 @@
_bus, match.c_str(),
[this](auto& msg) { this->handleTargetChange(msg); });
}
+
+ if (_errorDelay)
+ {
+ _errorTimer = std::make_unique<
+ sdeventplus::utility::Timer<sdeventplus::ClockId::Monotonic>>(
+ event, std::bind(&Fan::sensorErrorTimerExpired, &fan,
+ std::ref(*this)));
+ }
#ifndef MONITOR_USE_JSON
}
#endif
@@ -147,6 +157,23 @@
{
_functional = functional;
updateInventory(_functional);
+
+ if (!_errorTimer)
+ {
+ return;
+ }
+
+ if (!_functional)
+ {
+ if (_fan.present())
+ {
+ _errorTimer->restartOnce(std::chrono::seconds(*_errorDelay));
+ }
+ }
+ else if (_errorTimer->isEnabled())
+ {
+ _errorTimer->setEnabled(false);
+ }
}
/**
diff --git a/monitor/tach_sensor.hpp b/monitor/tach_sensor.hpp
index 27e0626..814df69 100644
--- a/monitor/tach_sensor.hpp
+++ b/monitor/tach_sensor.hpp
@@ -79,12 +79,16 @@
* @param[in] factor - the factor of the sensor target
* @param[in] offset - the offset of the sensor target
* @param[in] timeout - Normal timeout value to use
+ * @param[in] errorDelay - Delay in seconds before creating an error
+ * or std::nullopt if no errors.
+ *
* @param[in] event - Event loop reference
*/
TachSensor(Mode mode, sdbusplus::bus::bus& bus, Fan& fan,
const std::string& id, bool hasTarget, size_t funcDelay,
const std::string& interface, double factor, int64_t offset,
- size_t timeout, const sdeventplus::Event& event);
+ size_t timeout, const std::optional<size_t>& errorDelay,
+ const sdeventplus::Event& event);
/**
* @brief Returns the target speed value
@@ -186,6 +190,20 @@
return _name;
};
+ /**
+ * @brief Says if the error timer is running
+ *
+ * @return bool - If the timer is running
+ */
+ bool errorTimerRunning() const
+ {
+ if (_errorTimer && _errorTimer->isEnabled())
+ {
+ return true;
+ }
+ return false;
+ }
+
private:
/**
* @brief Returns the match string to use for matching
@@ -306,6 +324,24 @@
* @brief The match object for the Target properties changed signal
*/
std::unique_ptr<sdbusplus::server::match::match> targetSignal;
+
+ /**
+ * @brief The number of seconds to wait between a sensor being set
+ * to nonfunctional and creating an error for it.
+ *
+ * If std::nullopt, no errors will be created.
+ */
+ const std::optional<size_t> _errorDelay;
+
+ /**
+ * @brief The timer that uses _errorDelay. When it expires an error
+ * will be created for a faulted fan sensor (rotor).
+ *
+ * If _errorDelay is std::nullopt, then this won't be created.
+ */
+ std::unique_ptr<
+ sdeventplus::utility::Timer<sdeventplus::ClockId::Monotonic>>
+ _errorTimer;
};
} // namespace monitor
diff --git a/monitor/types.hpp b/monitor/types.hpp
index ff46fcd..8897d96 100644
--- a/monitor/types.hpp
+++ b/monitor/types.hpp
@@ -107,12 +107,14 @@
constexpr auto fanDeviationField = 3;
constexpr auto numSensorFailsForNonfuncField = 4;
constexpr auto monitorStartDelayField = 5;
-constexpr auto sensorListField = 6;
-constexpr auto conditionField = 7;
+constexpr auto nonfuncRotorErrDelayField = 6;
+constexpr auto sensorListField = 7;
+constexpr auto conditionField = 8;
using FanDefinition =
std::tuple<std::string, size_t, size_t, size_t, size_t, size_t,
- std::vector<SensorDefinition>, std::optional<Condition>>;
+ std::optional<size_t>, std::vector<SensorDefinition>,
+ std::optional<Condition>>;
constexpr auto presentHealthPos = 0;
constexpr auto sensorFuncHealthPos = 1;