dbus-sensors: RedfishSensor: Sync with upstream

This is a large change, but it adds another daemon to the suite of
existing dbus-sensors daemons, namely RedfishSensor.

This provides a service to read sensor data from a remote Redfish
server, in much the same way as other dbus-sensors daemons read their
sensor data from local hardware.

Input is D-Bus Inventory from entity-manager, for configuration.
Output is D-Bus Sensor objects, as with other dbus-sensors daemons.

Design doc:
https://gerrit.openbmc.org/c/openbmc/docs/+/58954

Configuration doc (instructions how to configure):
https://gerrit.openbmc.org/c/openbmc/docs/+/58955

Tested: Appropriately configured, it works. It pulls sensor data from
a live Redfish server, and makes it available locally. The data is
refreshed in a timely manner, and is suitable for use by consumers of
dbus-sensors Sensor objects, such as phosphor-pid-control for cooling.

Patch Tracking Bug: b/266550783
Upstream info / review: https://gerrit.openbmc.org/c/openbmc/dbus-sensors/+/60429
Upstream-Status: Submitted
Justification: In midst of iterative changes to address upstream concerns

Google-Bug-Id: 266550783
Signed-off-by: Josh Lehan <krellan@google.com>
Change-Id: Ia64cf33f447be339bec7b279351dcba0208a4281
(cherry picked from commit 4ba62792b108e221982c8bd319794afc6e93b3d9)
Signed-off-by: Josh Lehan <krellan@google.com>
diff --git a/recipes-phosphor/sensors/dbus-sensors/0101-RedfishSensor-It-reads-Redfish-sensors-to-D-Bus.patch b/recipes-phosphor/sensors/dbus-sensors/0101-RedfishSensor-It-reads-Redfish-sensors-to-D-Bus.patch
new file mode 100644
index 0000000..8f2a3dc
--- /dev/null
+++ b/recipes-phosphor/sensors/dbus-sensors/0101-RedfishSensor-It-reads-Redfish-sensors-to-D-Bus.patch
@@ -0,0 +1,4302 @@
+From 7c22294b8e43cc7063f6f401ee891a04642f4c29 Mon Sep 17 00:00:00 2001
+From: Josh Lehan <krellan@google.com>
+Date: Sun, 13 Nov 2022 15:14:48 -0800
+Subject: [PATCH] RedfishSensor: It reads Redfish sensors to D-Bus
+
+This is a large change, but it adds another daemon to the suite of
+existing dbus-sensors daemons, namely RedfishSensor.
+
+This provides a service to read sensor data from a remote Redfish
+server, in much the same way as other dbus-sensors daemons read their
+sensor data from local hardware.
+
+Input is D-Bus Inventory from entity-manager, for configuration.
+Output is D-Bus Sensor objects, as with other dbus-sensors daemons.
+
+Design doc:
+https://gerrit.openbmc.org/c/openbmc/docs/+/58954
+
+Configuration doc (instructions how to configure):
+https://gerrit.openbmc.org/c/openbmc/docs/+/58955
+
+Tested: Appropriately configured, it works. It pulls sensor data from
+a live Redfish server, and makes it available locally. The data is
+refreshed in a timely manner, and is suitable for use by consumers of
+dbus-sensors Sensor objects, such as phosphor-pid-control for cooling.
+
+Patch Tracking Bug: b/266550783
+Upstream info / review: https://gerrit.openbmc.org/c/openbmc/dbus-sensors/+/60429
+Upstream-Status: Submitted
+Justification: In midst of iterative changes to address upstream concerns
+
+Change-Id: I1c8fc0b2ffce6b0934838d416da5d01fabdbcc12
+Signed-off-by: Josh Lehan <krellan@google.com>
+---
+ include/RedfishSensor.hpp                     |  403 ++++++
+ meson_options.txt                             |    1 +
+ service_files/meson.build                     |    1 +
+ .../xyz.openbmc_project.redfishsensor.service |   13 +
+ src/RedfishSensor.cpp                         |  950 +++++++++++++
+ src/RedfishSensorMain.cpp                     |  628 +++++++++
+ src/RedfishSensorQuery.cpp                    |  915 ++++++++++++
+ src/RedfishSensorResponse.cpp                 | 1252 +++++++++++++++++
+ src/meson.build                               |   19 +
+ 9 files changed, 4182 insertions(+)
+ create mode 100644 include/RedfishSensor.hpp
+ create mode 100644 service_files/xyz.openbmc_project.redfishsensor.service
+ create mode 100644 src/RedfishSensor.cpp
+ create mode 100644 src/RedfishSensorMain.cpp
+ create mode 100644 src/RedfishSensorQuery.cpp
+ create mode 100644 src/RedfishSensorResponse.cpp
+
+diff --git a/include/RedfishSensor.hpp b/include/RedfishSensor.hpp
+new file mode 100644
+index 0000000..0fdb69a
+--- /dev/null
++++ b/include/RedfishSensor.hpp
+@@ -0,0 +1,403 @@
++#pragma once
++
++#include <Thresholds.hpp>
++#include <boost/asio/steady_timer.hpp>
++#include <boost/container/flat_map.hpp>
++#include <nlohmann/json.hpp>
++#include <sdbusplus/asio/connection.hpp>
++#include <sdbusplus/asio/object_server.hpp>
++#include <sensor.hpp>
++
++#include <limits>
++#include <memory>
++#include <string>
++#include <vector>
++
++// FUTURE: Use std::string when compiler allows constexpr
++class RedfishUnit
++{
++  public:
++    const char* const redfishType;
++    const char* const redfishUnits;
++    const char* const dbusUnits;
++    double rangeMin;
++    double rangeMax;
++};
++
++// FUTURE: Use std::string when compiler allows constexpr
++class RedfishThreshold
++{
++  public:
++    const char* const redfishName;
++    thresholds::Level level;
++    thresholds::Direction direction;
++};
++
++class RedfishUnitLookup
++{
++  public:
++    // FUTURE: Use std::vector when compiler allows constexpr
++    // This must exactly match unitTable in RedfishSensorResponse
++    std::array<RedfishUnit, 9> units;
++
++    const RedfishUnit& lookup(const std::string& name) const;
++};
++
++class RedfishThresholdLookup
++{
++  public:
++    // FUTURE: Use std::vector when compiler allows constexpr
++    // This must exactly match thresholdTable in RedfishSensorResponse
++    std::array<RedfishThreshold, 7> thresholds;
++
++    const RedfishThreshold& lookup(const std::string& name) const;
++};
++
++class RedfishCompletion
++{
++  public:
++    std::string queryRequest;
++    std::string queryResponse;
++
++    nlohmann::json jsonResponse;
++
++    int msElapsed = 0;
++    int errorCode = 0;
++};
++
++// Forward declaration;
++class RedfishConnection;
++
++// Encapsulates a Redfish transaction
++// This class does not need to be long-lived
++class RedfishTransaction :
++    public std::enable_shared_from_this<RedfishTransaction>
++{
++  public:
++    // Fill these in before calling submit()
++    std::string queryRequest;
++    std::function<void(const RedfishCompletion& response)> callbackResponse;
++
++    // Returns immediately, and also will call your callback later
++    void submitRequest(const std::shared_ptr<RedfishConnection>& connection);
++
++  private:
++    std::chrono::steady_clock::time_point timeStarted;
++
++    void handleCallback(const std::string& response, int code);
++};
++
++class RedfishSensorMatcher
++{
++  public:
++    std::string redfishName;
++    std::string redfishId;
++
++    bool isEmpty() const;
++    bool isMatch(const RedfishSensorMatcher& other) const;
++
++    void clear();
++    bool fillFromJson(const nlohmann::json& json);
++};
++
++class RedfishChassisMatcher
++{
++  public:
++    std::string redfishName;
++    std::string redfishId;
++    std::string manufacturer;
++    std::string model;
++    std::string partNumber;
++    std::string sku;
++    std::string serialNumber;
++    std::string sparePartNumber;
++    std::string version;
++
++    bool isEmpty() const;
++    bool isMatch(const RedfishChassisMatcher& other) const;
++
++    void clear();
++    bool fillFromJson(const nlohmann::json& json);
++};
++
++class RedfishSensorCandidate :
++    public std::enable_shared_from_this<RedfishSensorCandidate>
++{
++  public:
++    std::string sensorPath;
++    bool isAcceptable = false;
++
++    RedfishSensorMatcher characteristics;
++
++    // Avoids having to fetch the same Redfish URL twice during discovery
++    nlohmann::json readingCache;
++};
++
++// Unlike RedfishChassis, which comes from configuration, and can be used
++// across multiple servers, RedfishChassisCandidate comes from parsing
++// the JSON output of a server, and is owned by that individual server.
++class RedfishChassisCandidate :
++    public std::enable_shared_from_this<RedfishChassisCandidate>
++{
++  public:
++    std::string chassisPath;
++    std::string sensorsPath;
++    bool isAcceptable = false;
++
++    RedfishChassisMatcher characteristics;
++
++    std::vector<std::string> sensorPaths;
++    bool haveSensorPaths = false;
++
++    // For matching up sensors to this Chassis
++    boost::container::flat_map<std::string,
++                               std::shared_ptr<RedfishSensorCandidate>>
++        pathsToSensorCandidates;
++};
++
++class RedfishMetricReport :
++    public std::enable_shared_from_this<RedfishMetricReport>
++{
++  public:
++    std::string reportPath;
++
++    bool isHelpful = false;
++    bool isCollected = false;
++
++    // Avoids having to fetch the same Redfish URL twice during discovery
++    nlohmann::json reportCache;
++};
++
++// Forward declaration
++class RedfishSensor;
++
++class RedfishServer : public std::enable_shared_from_this<RedfishServer>
++{
++  public:
++    // Configuration fields
++    std::string configName;
++    std::string host;
++    std::string protocol;
++    std::string user;
++    std::string password;
++    int port = 0;
++
++    int readingsReaped = 0;
++    int readingsReports = 0;
++    int readingsGoodReported = 0;
++    int readingsGoodIndividual = 0;
++
++    int ticksElapsed = 0;
++    int ticksReading = 0;
++    int ticksLaggingBehind = 0;
++    int ticksDiscoveryStarting = 0;
++    int ticksDiscoveryContinuing = 0;
++
++    int64_t msNetworkTime = 0;
++    int64_t msReadingTime = 0;
++    int64_t msDiscoveryTime = 0;
++
++    int transactionsStarted = 0;
++    int transactionsSuccess = 0;
++    int transactionsFailure = 0;
++
++    int actionsDiscovery = 0;
++    int actionsReading = 0;
++    int completionsDiscovery = 0;
++    int completionsReading = 0;
++
++    // Sensors that are configured to use this Server
++    std::vector<std::shared_ptr<RedfishSensor>> sensorsServed;
++
++    bool isRelevant = false;
++
++    void staleReaper(const std::chrono::steady_clock::time_point& now);
++
++    void provideContext(std::shared_ptr<boost::asio::io_service> io,
++                        std::shared_ptr<sdbusplus::asio::connection> conn,
++                        std::shared_ptr<sdbusplus::asio::object_server> obj);
++
++    void startTimer();
++    void handleTimer();
++
++    void startNetworking();
++    void nextAction();
++
++  private:
++    // Optional because needs context passed in at construction time
++    std::optional<boost::asio::steady_timer> pollTimer;
++    std::shared_ptr<RedfishConnection> networkConnection;
++
++    // Necessary to hold on to, to create RedfishSensorImpl when ready
++    std::shared_ptr<boost::asio::io_service> ioContext;
++    std::shared_ptr<sdbusplus::asio::connection> dbusConnection;
++    std::shared_ptr<sdbusplus::asio::object_server> objectServer;
++
++    // Information learned from Redfish during discovery
++    std::string pathChassis;
++    std::string pathTelemetry;
++    std::string pathMetrics;
++    std::vector<std::string> reportPaths;
++    std::vector<std::string> chassisPaths;
++    bool haveReportPaths = false;
++    bool haveChassisPaths = false;
++
++    // For matching up sensors by name during fast path
++    boost::container::flat_map<std::string, std::shared_ptr<RedfishSensor>>
++        pathsToSensors;
++
++    // For matching up sensors to their intended Chassis on this server
++    boost::container::flat_map<std::string,
++                               std::shared_ptr<RedfishChassisCandidate>>
++        pathsToChassisCandidates;
++
++    // For keeping track of each report gathered during reading
++    boost::container::flat_map<std::string,
++                               std::shared_ptr<RedfishMetricReport>>
++        pathsToMetricReports;
++
++    // For remembering starting times to measure how long it took
++    std::chrono::steady_clock::time_point timeDiscovery;
++    std::chrono::steady_clock::time_point timeReading;
++    std::chrono::steady_clock::time_point timeAction;
++
++    bool stubActivated = false;
++
++    // Network state
++    bool discoveryDone = false;
++    bool networkBusy = false;
++
++    std::shared_ptr<RedfishTransaction> activeTransaction;
++
++    void advanceDiscovery();
++    void acceptSensors();
++    void readSensors();
++    void advanceReading();
++
++    // Transaction bookends
++    bool sendTransaction(const RedfishTransaction& transaction);
++    bool doneTransaction(const RedfishCompletion& completion);
++
++    // Parsers of JSON obtained from Redfish
++    bool fillFromRoot(const nlohmann::json& json);
++    bool fillFromTelemetry(const nlohmann::json& json);
++
++    bool fillFromMetricCollection(const nlohmann::json& json);
++    bool fillFromMetricReport(const nlohmann::json& json);
++
++    bool fillFromChassisCollection(const nlohmann::json& json);
++    bool fillFromChassisCandidate(const nlohmann::json& json,
++                                  const std::string& path);
++
++    bool fillFromSensorCollection(const nlohmann::json& json,
++                                  const std::string& path);
++    bool fillFromSensorCandidate(const nlohmann::json& json,
++                                 const std::string& path);
++
++    bool fillFromSensorAccepted(const nlohmann::json& json);
++
++    int checkMetricReport(const nlohmann::json& json, int& outMatched);
++
++    bool acceptMetricReport(const nlohmann::json& json,
++                            const std::chrono::steady_clock::time_point& now);
++    bool acceptSensorReading(const nlohmann::json& json,
++                             const std::chrono::steady_clock::time_point& now);
++
++    // Queriers to initiate new Redfish network communication
++    void queryRoot();
++    void queryTelemetry();
++    void queryMetricCollection();
++    void queryMetricReport(const std::string& path);
++    void queryChassisCollection();
++    void queryChassisCandidate(const std::string& path);
++    void querySensorCollection(const std::string& path);
++    void querySensorCandidate(const std::string& path);
++    void collectMetricReport(const std::string& path);
++    void collectSensorReading(const std::string& path);
++};
++
++// Forward declaration
++class RedfishSensor;
++
++// This object comes from entity-manager configuration, and it is allowed
++// to use the same RedfishChassis with multiple Redfish servers.
++class RedfishChassis : public std::enable_shared_from_this<RedfishChassis>
++{
++  public:
++    // Configuration fields
++    std::string configName;
++    RedfishChassisMatcher characteristics;
++
++    // State
++    bool isRelevant = false;
++
++    // Sensors that are configured to use this Chassis
++    std::vector<std::shared_ptr<RedfishSensor>> sensorsContained;
++};
++
++// Separate object because cannot be created until info learned from server
++class RedfishSensorImpl :
++    public Sensor,
++    public std::enable_shared_from_this<RedfishSensorImpl>
++{
++  public:
++    RedfishSensorImpl(const std::string& objectType,
++                      sdbusplus::asio::object_server& objectServer,
++                      std::shared_ptr<sdbusplus::asio::connection>& conn,
++                      const std::string& sensorName,
++                      const std::string& sensorUnits,
++                      std::vector<thresholds::Threshold>&& thresholdsIn,
++                      const std::string& sensorConfiguration, double maxReading,
++                      double minReading, const PowerState& powerState);
++    ~RedfishSensorImpl() override;
++
++  private:
++    sdbusplus::asio::object_server& objectServer;
++
++    void checkThresholds(void) override;
++};
++
++class RedfishSensor : public std::enable_shared_from_this<RedfishSensor>
++{
++  public:
++    // Configuration fields
++    std::string configName;
++    RedfishSensorMatcher characteristics;
++
++    std::string configChassis;
++    std::string configServer;
++    PowerState powerState = PowerState::always;
++
++    // Learned during initialization
++    std::string inventoryPath;
++
++    // Learned from matched Redfish object
++    std::string chassisPath;
++    std::string redfishPath;
++
++    // Populated by information from matched Redfish object
++    std::string units;
++    double minValue = std::numeric_limits<double>::quiet_NaN();
++    double maxValue = std::numeric_limits<double>::quiet_NaN();
++    std::vector<thresholds::Threshold> thresholds;
++
++    // The last known good reading
++    double readingValue = std::numeric_limits<double>::quiet_NaN();
++    std::chrono::steady_clock::time_point readingWhen;
++
++    // Sensor reading state
++    bool isRelevant = false;
++    bool isCollected = false;
++
++    // Do not call this until enough information learned from Redfish
++    // The underlying Sensor constructor requires dbusConn non-const ref
++    void createImpl(
++        const std::shared_ptr<sdbusplus::asio::object_server>& objServer,
++        std::shared_ptr<sdbusplus::asio::connection>& dbusConn);
++
++    std::shared_ptr<RedfishSensorImpl> impl;
++
++    // Server and Chassis that this sensor is configured to use
++    std::shared_ptr<RedfishServer> server;
++    std::shared_ptr<RedfishChassis> chassis;
++};
+diff --git a/meson_options.txt b/meson_options.txt
+index eba6b68..2d96c0a 100644
+--- a/meson_options.txt
++++ b/meson_options.txt
+@@ -10,6 +10,7 @@ option('mcu', type: 'feature', value: 'enabled', description: 'Enable MCU sensor
+ option('nvme', type: 'feature', value: 'enabled', description: 'Enable NVMe sensor.',)
+ option('psu', type: 'feature', value: 'enabled', description: 'Enable PSU sensor.',)
+ option('external', type: 'feature', value: 'enabled', description: 'Enable External sensor.',)
++option('redfish', type: 'feature', value: 'enabled', description: 'Enable Redfish sensor.',)
+ option('tests', type: 'feature', value: 'enabled', description: 'Build tests.',)
+ option('validate-unsecure-feature', type : 'feature', value : 'disabled', description : 'Enables unsecure features required by validation. Note: mustbe turned off for production images.',)
+ option('insecure-sensor-override', type : 'feature', value : 'disabled', description : 'Enables Sensor override feature without any check.',)
+diff --git a/service_files/meson.build b/service_files/meson.build
+index 4d9e106..0e5ad8f 100644
+--- a/service_files/meson.build
++++ b/service_files/meson.build
+@@ -11,6 +11,7 @@ unit_files = [
+     ['nvme', 'xyz.openbmc_project.nvmesensor.service'],
+     ['psu', 'xyz.openbmc_project.psusensor.service'],
+     ['external', 'xyz.openbmc_project.externalsensor.service'],
++    ['redfish', 'xyz.openbmc_project.redfishsensor.service'],
+ ]
+ 
+ foreach tuple : unit_files
+diff --git a/service_files/xyz.openbmc_project.redfishsensor.service b/service_files/xyz.openbmc_project.redfishsensor.service
+new file mode 100644
+index 0000000..2a749e6
+--- /dev/null
++++ b/service_files/xyz.openbmc_project.redfishsensor.service
+@@ -0,0 +1,13 @@
++[Unit]
++Description=Redfish Sensor
++StopWhenUnneeded=false
++Requires=xyz.openbmc_project.EntityManager.service
++After=xyz.openbmc_project.EntityManager.service
++
++[Service]
++Restart=always
++RestartSec=5
++ExecStart=/usr/bin/redfishsensor
++
++[Install]
++WantedBy=multi-user.target
+diff --git a/src/RedfishSensor.cpp b/src/RedfishSensor.cpp
+new file mode 100644
+index 0000000..96b2066
+--- /dev/null
++++ b/src/RedfishSensor.cpp
+@@ -0,0 +1,950 @@
++#include <RedfishSensor.hpp>
++#include <SensorPaths.hpp>
++#include <boost/asio/steady_timer.hpp>
++#include <boost/container/flat_map.hpp>
++#include <sdbusplus/asio/connection.hpp>
++#include <sdbusplus/asio/object_server.hpp>
++
++#include <chrono>
++#include <string>
++#include <utility>
++#include <vector>
++
++static constexpr bool debug = false;
++
++static constexpr int pollIntervalTimeMs = 1000;
++static constexpr int staleSensorTimeMs = 4000;
++
++const RedfishUnit& RedfishUnitLookup::lookup(const std::string& name) const
++{
++    // Matching on any string is OK, there is no overlap
++    for (const auto& unit : units)
++    {
++        if (unit.redfishType == name)
++        {
++            return unit;
++        }
++        if (unit.redfishUnits == name)
++        {
++            return unit;
++        }
++        if (unit.dbusUnits == name)
++        {
++            return unit;
++        }
++    }
++
++    // Indicate error by returning the padding "end" element
++    return *(units.crbegin());
++}
++
++const RedfishThreshold&
++    RedfishThresholdLookup::lookup(const std::string& name) const
++{
++    for (const auto& threshold : thresholds)
++    {
++        if (threshold.redfishName == name)
++        {
++            return threshold;
++        }
++    }
++
++    // Indicate error by returning the padding "end" element
++    return *(thresholds.crbegin());
++}
++
++static bool characteristicMatches(const std::string& a, const std::string& b)
++{
++    // If empty, compare as "don't care", so will always match then
++    if (a.empty() || b.empty())
++    {
++        return true;
++    }
++
++    // Both are not empty, so must have same content, in order to return true
++    return (a == b);
++}
++
++bool RedfishChassisMatcher::isEmpty() const
++{
++    // All must be empty, in order to return true
++    return (redfishName.empty() && redfishId.empty() && manufacturer.empty() &&
++            model.empty() && partNumber.empty() && sku.empty() &&
++            serialNumber.empty() && sparePartNumber.empty() && version.empty());
++}
++
++bool RedfishChassisMatcher::isMatch(const RedfishChassisMatcher& other) const
++{
++    // It must have at least one characteristic to match on
++    if (isEmpty())
++    {
++        return false;
++    }
++    if (other.isEmpty())
++    {
++        return false;
++    }
++
++    // All characteristics must match, in order to return true
++    if (characteristicMatches(redfishName, other.redfishName) &&
++        characteristicMatches(redfishId, other.redfishId) &&
++        characteristicMatches(manufacturer, other.manufacturer) &&
++        characteristicMatches(model, other.model) &&
++        characteristicMatches(partNumber, other.partNumber) &&
++        characteristicMatches(sku, other.sku) &&
++        characteristicMatches(serialNumber, other.serialNumber) &&
++        characteristicMatches(sparePartNumber, other.sparePartNumber) &&
++        characteristicMatches(version, other.version))
++    {
++        if constexpr (debug)
++        {
++            std::cerr << "Chassises match: name " << redfishName << ", id "
++                      << redfishId << " and name " << other.redfishName
++                      << ", id " << other.redfishId << "\n";
++        }
++        return true;
++    }
++
++    return false;
++}
++
++void RedfishChassisMatcher::clear()
++{
++    redfishName.clear();
++    redfishId.clear();
++
++    manufacturer.clear();
++    model.clear();
++    partNumber.clear();
++    sku.clear();
++    serialNumber.clear();
++    sparePartNumber.clear();
++    version.clear();
++}
++
++bool RedfishSensorMatcher::isEmpty() const
++{
++    // All must be empty, in order to return true
++    return (redfishName.empty() && redfishId.empty());
++}
++
++bool RedfishSensorMatcher::isMatch(const RedfishSensorMatcher& other) const
++{
++    // It must have at least one characteristic to match on
++    if (isEmpty())
++    {
++        return false;
++    }
++    if (other.isEmpty())
++    {
++        return false;
++    }
++
++    // All characteristics must match, in order to return true
++    if (characteristicMatches(redfishName, other.redfishName) &&
++        characteristicMatches(redfishId, other.redfishId))
++    {
++        if constexpr (debug)
++        {
++            std::cerr << "Sensors match: name " << redfishName << ", id "
++                      << redfishId << " and name " << other.redfishName
++                      << ", id " << other.redfishId << "\n";
++        }
++        return true;
++    }
++
++    return false;
++}
++
++void RedfishSensorMatcher::clear()
++{
++    redfishName.clear();
++    redfishId.clear();
++}
++
++void RedfishServer::staleReaper(
++    const std::chrono::steady_clock::time_point& now)
++{
++    for (const auto& sensorPtr : sensorsServed)
++    {
++        RedfishSensor& sensor = *sensorPtr;
++
++        if (!(sensor.isRelevant))
++        {
++            continue;
++        }
++
++        if (!(sensor.impl))
++        {
++            // Sensor has not completed initialization yet
++            continue;
++        }
++
++        // This is intentionally isnan(), not isfinite()
++        if (std::isnan(sensor.readingValue))
++        {
++            // It's already dead
++            continue;
++        }
++
++        auto msAge = std::chrono::duration_cast<std::chrono::milliseconds>(
++                         now - sensor.readingWhen)
++                         .count();
++
++        if (msAge < staleSensorTimeMs)
++        {
++            // It's still alive
++            continue;
++        }
++
++        std::cerr << "Sensor " << sensor.configName
++                  << " reading has gone stale: " << sensor.readingValue
++                  << " value, " << msAge << " ms age\n";
++
++        sensor.readingValue = std::numeric_limits<double>::quiet_NaN();
++        sensor.readingWhen = now;
++        sensor.impl->updateValue(sensor.readingValue);
++
++        ++readingsReaped;
++    }
++}
++
++RedfishSensorImpl::RedfishSensorImpl(
++    const std::string& objectTypeIn,
++    sdbusplus::asio::object_server& objectServerIn,
++    std::shared_ptr<sdbusplus::asio::connection>& dbusConn,
++    const std::string& sensorName, const std::string& sensorUnits,
++    std::vector<thresholds::Threshold>&& thresholdsIn,
++    const std::string& sensorConfiguration, double maxReading,
++    double minReading, const PowerState& powerState) :
++    // FUTURE: Verify name is not double-escaped, escapeName() here,
++    // but then sensor_paths::escapePathForDbus() in base class
++    Sensor(escapeName(sensorName), std::move(thresholdsIn), sensorConfiguration,
++           objectTypeIn, false, false, maxReading, minReading, dbusConn,
++           powerState),
++    objectServer(objectServerIn)
++{
++    // Like ExternalSensor, this class can represent any type of sensor,
++    // so caller must specify the units it will be physically measuring.
++    std::string dbusPath = sensor_paths::getPathForUnits(sensorUnits);
++    if (dbusPath.empty())
++    {
++        throw std::runtime_error("Units not in allow list");
++    }
++
++    std::string objectPath = "/xyz/openbmc_project/sensors/";
++    objectPath += dbusPath;
++    objectPath += '/';
++    objectPath += sensorName;
++
++    sensorInterface = objectServer.add_interface(
++        objectPath, "xyz.openbmc_project.Sensor.Value");
++
++    for (const auto& threshold : thresholds)
++    {
++        std::string interface = thresholds::getInterface(threshold.level);
++        thresholdInterfaces[static_cast<size_t>(threshold.level)] =
++            objectServer.add_interface(objectPath, interface);
++    }
++
++    association =
++        objectServer.add_interface(objectPath, association::interface);
++    setInitialProperties(sensorUnits);
++
++    if constexpr (debug)
++    {
++        std::cerr << "RedfishSensor: Constructed " << name << ", config "
++                  << configurationPath << ", type " << objectType << ", path "
++                  << objectPath << ", type " << objectType << ", min "
++                  << minReading << ", max " << maxReading << "\n";
++    }
++}
++
++RedfishSensorImpl::~RedfishSensorImpl()
++{
++    objectServer.remove_interface(association);
++    for (const auto& iface : thresholdInterfaces)
++    {
++        objectServer.remove_interface(iface);
++    }
++    objectServer.remove_interface(sensorInterface);
++
++    if constexpr (debug)
++    {
++        std::cerr << "RedfishServer: Destructed " << name << "\n";
++    }
++}
++
++void RedfishSensorImpl::checkThresholds()
++{
++    thresholds::checkThresholds(this);
++}
++
++void RedfishSensor::createImpl(
++    const std::shared_ptr<sdbusplus::asio::object_server>& objServer,
++    std::shared_ptr<sdbusplus::asio::connection>& dbusConn)
++{
++    if constexpr (debug)
++    {
++        std::cerr << "RedfishSensor: Creating D-Bus object for " << configName
++                  << "\n";
++    }
++
++    auto thresholdsCopy = thresholds;
++
++    impl.reset();
++
++    impl = std::make_shared<RedfishSensorImpl>(
++        "RedfishSensor", *objServer, dbusConn, configName, units,
++        std::move(thresholdsCopy), inventoryPath, maxValue, minValue,
++        powerState);
++
++    if constexpr (debug)
++    {
++        std::cerr << "Created successfully\n";
++    }
++}
++
++static void timerCallback(const std::weak_ptr<RedfishServer>& that,
++                          const boost::system::error_code& ec)
++{
++    if (ec)
++    {
++        std::cerr << "Timer callback error: " << ec.message();
++        return;
++    }
++
++    auto lockThat = that.lock();
++    if (lockThat)
++    {
++        lockThat->handleTimer();
++        return;
++    }
++
++    std::cerr << "Timer callback ignored: Server object has disappeared\n";
++}
++
++void RedfishServer::startTimer()
++{
++    // Timer does not need kicking off again if already initialized
++    if (pollTimer)
++    {
++        std::cerr << "Server " << configName << " already initialized\n";
++        return;
++    }
++
++    pollTimer.emplace(*ioContext);
++    std::chrono::steady_clock::time_point when =
++        std::chrono::steady_clock::now();
++    when += std::chrono::milliseconds(pollIntervalTimeMs);
++
++    auto weakThis = weak_from_this();
++
++    pollTimer->expires_at(when);
++    pollTimer->async_wait([weakThis](const boost::system::error_code& ec) {
++        timerCallback(weakThis, ec);
++    });
++
++    if constexpr (debug)
++    {
++        std::cerr << "Server " << configName << " timer ready\n";
++    }
++}
++
++// This will check state and kick off the next network action,
++// as it is called from the timer callback handler.
++void RedfishServer::nextAction()
++{
++    // Avoid doubling-up work queues by not submitting more if already busy
++    if (networkBusy)
++    {
++        // It is normal to be continuously busy during discovery
++        if (discoveryDone)
++        {
++            // But not normal to still have prior tick going during reading
++            ++ticksLaggingBehind;
++            if constexpr (debug)
++            {
++                std::cerr << "Server " << configName
++                          << " timer tick, lagging behind: "
++                          << ticksLaggingBehind << " ticks\n";
++            }
++        }
++        else
++        {
++            ++ticksDiscoveryContinuing;
++            if constexpr (debug)
++            {
++                std::cerr << "Server " << configName
++                          << " timer tick, discovery continuing: "
++                          << ticksDiscoveryContinuing << " ticks\n";
++            }
++        }
++        return;
++    }
++
++    // Do discovery, in lieu of reading, until discovery all done
++    if (!discoveryDone)
++    {
++        ++ticksDiscoveryStarting;
++        timeDiscovery = timeAction;
++
++        if constexpr (debug)
++        {
++            std::cerr << "Server " << configName
++                      << " timer tick, discovery starting: "
++                      << ticksDiscoveryStarting << " ticks\n";
++        }
++
++        advanceDiscovery();
++        return;
++    }
++
++    ++ticksReading;
++    timeReading = timeAction;
++
++    if constexpr (debug)
++    {
++        std::cerr << "Server " << configName
++                  << " timer tick, reading sensors: " << ticksReading
++                  << " ticks\n";
++    }
++
++    readSensors();
++}
++
++void RedfishServer::handleTimer()
++{
++    std::chrono::steady_clock::time_point now =
++        std::chrono::steady_clock::now();
++    std::chrono::steady_clock::time_point when = pollTimer->expiry();
++
++    ++ticksElapsed;
++
++    timeAction = now;
++
++    // Initiates the next network activity, unless network is already busy
++    // Keeps track of how many times we had to skip because network was busy
++    nextAction();
++
++    // Expire stale sensors
++    staleReaper(timeAction);
++
++    auto weakThis = weak_from_this();
++
++    when += std::chrono::milliseconds(pollIntervalTimeMs);
++
++    // This is explicitly not expires_from_now(). Instead, the expiration
++    // time is incremented manually, to ensure accurate intervals, that do
++    // not drift slower over time due to processing overhead.
++    pollTimer->expires_at(when);
++
++    pollTimer->async_wait([weakThis](const boost::system::error_code& ec) {
++        timerCallback(weakThis, ec);
++    });
++
++    if constexpr (debug)
++    {
++        // If lagging, "now" will get ahead of "when"
++        auto msLag =
++            std::chrono::duration_cast<std::chrono::milliseconds>(now - when)
++                .count();
++
++        std::chrono::steady_clock::time_point later =
++            std::chrono::steady_clock::now();
++
++        auto msProc =
++            std::chrono::duration_cast<std::chrono::milliseconds>(later - now)
++                .count();
++
++        std::cerr << "Server " << configName << " timer: " << ticksElapsed
++                  << " ticks, lagging " << msLag << " ms, processing " << msProc
++                  << " ms\n";
++    }
++}
++
++void RedfishServer::provideContext(
++    std::shared_ptr<boost::asio::io_service> io,
++    std::shared_ptr<sdbusplus::asio::connection> conn,
++    std::shared_ptr<sdbusplus::asio::object_server> obj)
++{
++    // Have to hold onto these, needed for various I/O during operation
++    ioContext = std::move(io);
++    objectServer = std::move(obj);
++    dbusConnection = std::move(conn);
++}
++
++void RedfishServer::advanceDiscovery()
++{
++    // This can be called multiple times per tick
++    ++actionsDiscovery;
++
++    // Learn where Chassis is
++    if (pathChassis.empty())
++    {
++        // Fills Chassis, and maybe also Telemetry
++        queryRoot();
++        return;
++    }
++
++    // Optionally might have learned where Telemetry is
++    if (!(pathTelemetry.empty()))
++    {
++        // Continue to learning where MetricReports is
++        if (pathMetrics.empty())
++        {
++            // Fills Metrics
++            queryTelemetry();
++            return;
++        }
++
++        // Continue to filling in the list of reports
++        if (!haveReportPaths)
++        {
++            // Fills MetricReportPaths
++            queryMetricCollection();
++            return;
++        }
++
++        // Collect all metric reports, for use during preflight
++        for (const std::string& reportPath : reportPaths)
++        {
++            auto iter = pathsToMetricReports.find(reportPath);
++            if (iter == pathsToMetricReports.end())
++            {
++                // Fills in a pathsToMetricReports element
++                queryMetricReport(reportPath);
++                return;
++            }
++        }
++    }
++
++    // Continue to filling in the list of chassises
++    if (!haveChassisPaths)
++    {
++        // Fills ChassisPaths
++        queryChassisCollection();
++        return;
++    }
++
++    // Ask chassis candidates where their sensors are
++    // Query one chassis candidate per pass, until all are queried
++    for (const std::string& chassisPath : chassisPaths)
++    {
++        auto iter = pathsToChassisCandidates.find(chassisPath);
++        if (iter == pathsToChassisCandidates.end())
++        {
++            // Fills in a pathsToChassisCandidates element
++            queryChassisCandidate(chassisPath);
++            return;
++        }
++    }
++
++    // Fill in the list of available sensors on each chassis
++    // Query one chassis candidate per pass, until all are queried
++    for (const std::string& chassisPath : chassisPaths)
++    {
++        auto iterCha = pathsToChassisCandidates.find(chassisPath);
++        if (iterCha == pathsToChassisCandidates.end())
++        {
++            std::cerr
++                << "Internal error: Chassis candidate was never filled in\n";
++
++            // Should not happen if previous query was successful
++            continue;
++        }
++
++        RedfishChassisCandidate& candCha = *(iterCha->second);
++
++        if (!(candCha.isAcceptable))
++        {
++            // This chassis is no longer interesting to us
++            continue;
++        }
++
++        std::string sensorsPath = candCha.sensorsPath;
++        if (sensorsPath.empty())
++        {
++            // This chassis contains no sensors
++            continue;
++        }
++
++        if (candCha.haveSensorPaths)
++        {
++            // This chassis already completed this pass
++            continue;
++        }
++
++        // Fills in this chassis candidate's sensorPaths vector
++        querySensorCollection(sensorsPath);
++        return;
++    }
++
++    // Look at sensor candidates on all chassis candidates
++    // Query one sensor candidate per pass, until all are queried
++    for (const std::string& chassisPath : chassisPaths)
++    {
++        auto iterCha = pathsToChassisCandidates.find(chassisPath);
++        if (iterCha == pathsToChassisCandidates.end())
++        {
++            std::cerr
++                << "Internal error: Chassis candidate was never filled in\n";
++
++            // Should not happen if previous query was successful
++            continue;
++        }
++
++        RedfishChassisCandidate& candCha = *(iterCha->second);
++
++        if (!(candCha.isAcceptable))
++        {
++            // This chassis is no longer interesting to us
++            continue;
++        }
++
++        for (const std::string& sensorPath : candCha.sensorPaths)
++        {
++            auto iterSens = candCha.pathsToSensorCandidates.find(sensorPath);
++            if (iterSens == candCha.pathsToSensorCandidates.end())
++            {
++                // Fills in pathToSensorCandidates for this sensorPath element
++                querySensorCandidate(sensorPath);
++                return;
++            }
++        }
++    }
++
++    // At this point, all necessary information has been filled in
++    acceptSensors();
++
++    discoveryDone = true;
++
++    auto now = std::chrono::steady_clock::now();
++    auto msTaken = std::chrono::duration_cast<std::chrono::milliseconds>(
++                       now - timeDiscovery)
++                       .count();
++
++    // There should be only one discovery cycle if all goes well
++    msDiscoveryTime += msTaken;
++    ++completionsDiscovery;
++
++    if constexpr (debug)
++    {
++        double average = static_cast<double>(msDiscoveryTime);
++        average /= static_cast<double>(completionsDiscovery);
++        auto msAverage = static_cast<int>(average);
++
++        std::cerr << "Discovery done, " << msTaken << " ms, " << msAverage
++                  << " ms average\n";
++    }
++
++    if constexpr (debug)
++    {
++        std::cerr << "Redfish discovery complete: " << actionsDiscovery
++                  << " actions, " << completionsDiscovery
++                  << " completions, ticks " << ticksDiscoveryStarting
++                  << " starts, " << ticksDiscoveryContinuing << " continues\n";
++    }
++}
++
++void RedfishServer::acceptSensors()
++{
++    if constexpr (debug)
++    {
++        std::cerr << "Ready to finalize what was discovered\n";
++    }
++
++    // FUTURE: Make sure this works after accepting sensorsChanged
++    for (auto& sensorPair : pathsToSensors)
++    {
++        sensorPair.second.reset();
++    }
++    pathsToSensors.clear();
++
++    size_t countDiscovered = 0;
++    size_t countAlready = 0;
++    size_t countNotRelevant = 0;
++    size_t countAmbiguous = 0;
++    size_t countNotFound = 0;
++    size_t countIncomplete = 0;
++
++    // This is the meat of the discovery algorithm, and puts the pieces
++    // together, filling in pathsToSensors and instantiating sensor objects
++    for (const auto& sensorPtr : sensorsServed)
++    {
++        RedfishSensor& sensor = *sensorPtr;
++
++        if (!(sensor.isRelevant))
++        {
++            ++countNotRelevant;
++            continue;
++        }
++
++        // Do not discover again if already instantiated
++        if (sensor.impl)
++        {
++            ++countAlready;
++            continue;
++        }
++
++        size_t numChaMatches = 0;
++        std::shared_ptr<RedfishChassisCandidate> chaCandPtr;
++
++        // Exactly one relevant Chassis must match desired characteristics
++        for (const auto& chaCandPair : pathsToChassisCandidates)
++        {
++            RedfishChassisCandidate& chaCand = *(chaCandPair.second);
++
++            if (!(chaCand.isAcceptable))
++            {
++                continue;
++            }
++
++            if (sensor.chassis->characteristics.isMatch(
++                    chaCand.characteristics))
++            {
++                ++numChaMatches;
++                chaCandPtr = chaCandPair.second;
++
++                if constexpr (debug)
++                {
++                    std::cerr << "Chassis " << sensor.chassis->configName
++                              << " likely is " << chaCand.chassisPath << "\n";
++                }
++            }
++        }
++
++        if (numChaMatches > 1)
++        {
++            std::cerr << "Chassis " << sensor.chassis->configName
++                      << " configuration is ambiguous, unable to narrow down "
++                         "which one it was on Redfish server "
++                      << configName << "\n";
++
++            ++countAmbiguous;
++            continue;
++        }
++
++        if (numChaMatches < 1)
++        {
++            std::cerr << "Chassis " << sensor.chassis->configName
++                      << " was not found on Redfish server " << configName
++                      << "\n";
++
++            ++countNotFound;
++            continue;
++        }
++
++        // At this point, the Chassis is known, now look through its sensors
++        size_t numSensMatches = 0;
++        std::shared_ptr<RedfishSensorCandidate> sensCandPtr;
++
++        // Exactly one relevant Sensor on this Chassis must match, similarly
++        for (const auto& sensCandPair : chaCandPtr->pathsToSensorCandidates)
++        {
++            RedfishSensorCandidate& sensCand = *(sensCandPair.second);
++
++            if (!(sensCand.isAcceptable))
++            {
++                continue;
++            }
++
++            if (sensor.characteristics.isMatch(sensCand.characteristics))
++            {
++                ++numSensMatches;
++                sensCandPtr = sensCandPair.second;
++
++                if constexpr (debug)
++                {
++                    std::cerr << "Sensor " << sensor.configName << " likely is "
++                              << sensCand.sensorPath << "\n";
++                }
++            }
++        }
++
++        if (numSensMatches > 1)
++        {
++            std::cerr << "Sensor " << sensor.configName
++                      << " configuration is ambiguous, unable to narrow down "
++                         "which one it was in Redfish chassis "
++                      << sensor.chassis->configName << "\n";
++
++            ++countAmbiguous;
++            continue;
++        }
++
++        if (numSensMatches < 1)
++        {
++            std::cerr << "Sensor " << sensor.configName
++                      << " was not found in Redfish chassis "
++                      << sensor.chassis->configName << "\n";
++
++            ++countNotFound;
++            continue;
++        }
++
++        // Achieved a match, cache learned Redfish paths for fast lookup later
++        // Fill in what is necessary to call fillFromSensorAccepted()
++        sensor.redfishPath = sensCandPtr->sensorPath;
++        sensor.chassisPath = chaCandPtr->chassisPath;
++        pathsToSensors[sensor.redfishPath] = sensorPtr;
++
++        // Now we know which sensor to parse this learned information into
++        if (!(fillFromSensorAccepted(sensCandPtr->readingCache)))
++        {
++            std::cerr << "Sensor " << sensor.configName
++                      << " has incomplete information on Redfish server "
++                      << configName << "\n";
++
++            // Not enough information to call createImpl
++            ++countIncomplete;
++            continue;
++        }
++
++        // This sensor now is completely learned and ready to instantiate
++        sensor.createImpl(objectServer, dbusConnection);
++
++        std::cerr << "Found " << sensor.configName << " at "
++                  << sensor.redfishPath << "\n";
++
++        ++countDiscovered;
++    }
++
++    // Now that sensors are sorted, see which MetricReports they go with
++    for (auto& metricReportPair : pathsToMetricReports)
++    {
++        RedfishMetricReport& metricReport = *(metricReportPair.second);
++
++        metricReport.isHelpful = false;
++
++        int sensorsIncluded = 0;
++        int reportsPresent =
++            checkMetricReport(metricReport.reportCache, sensorsIncluded);
++
++        if (sensorsIncluded < 1)
++        {
++            std::cerr << "Report " << metricReport.reportPath
++                      << " not relevant for any of our sensors\n";
++            continue;
++        }
++        if (reportsPresent < sensorsIncluded)
++        {
++            std::cerr << "Report " << metricReport.reportPath
++                      << " not usable with our sensors\n";
++            continue;
++        }
++
++        // This report will help us accelerate sensor data collection
++        metricReport.isHelpful = true;
++
++        if constexpr (debug)
++        {
++            std::cerr << "Report " << metricReport.reportPath
++                      << " is useful: " << reportsPresent << " readings, "
++                      << sensorsIncluded << " relevant\n";
++        }
++    }
++
++    size_t countBad =
++        countNotRelevant + countAmbiguous + countNotFound + countIncomplete;
++
++    std::cerr << "Server communicating: " << countDiscovered
++              << " sensors found, " << countAlready << " previous, " << countBad
++              << " failed\n";
++    if (countBad > 0)
++    {
++        std::cerr << "Of those that failed: " << countNotRelevant
++                  << " config problem, " << countAmbiguous << " ambiguous, "
++                  << countNotFound << " not found, " << countIncomplete
++                  << " server problem\n";
++    }
++}
++
++void RedfishServer::readSensors()
++{
++    // Begin new reading cycle by marking all as not collected yet
++    for (auto& metricReportPair : pathsToMetricReports)
++    {
++        metricReportPair.second->isCollected = false;
++    }
++    for (auto& sensorPair : pathsToSensors)
++    {
++        sensorPair.second->isCollected = false;
++    }
++
++    advanceReading();
++}
++
++void RedfishServer::advanceReading()
++{
++    // This can be called multiple times per tick
++    ++actionsReading;
++
++    // First, gather all MetricReports that are known to be helpful
++    for (const auto& metricReportPair : pathsToMetricReports)
++    {
++        RedfishMetricReport& metricReport = *(metricReportPair.second);
++
++        if (metricReport.isCollected)
++        {
++            // Already collected
++            continue;
++        }
++
++        if (!(metricReport.isHelpful))
++        {
++            // This report would not be helpful to us, even if collected
++            continue;
++        }
++
++        collectMetricReport(metricReport.reportPath);
++        return;
++    }
++
++    // Second, gather individual Sensor objects not included in reports
++    for (const auto& sensorPair : pathsToSensors)
++    {
++        RedfishSensor& sensor = *(sensorPair.second);
++
++        if (sensor.isCollected)
++        {
++            // Already collected
++            continue;
++        }
++
++        if (!(sensor.isRelevant))
++        {
++            // This sensor was already deemed not relevant to us
++            continue;
++        }
++
++        if (!(sensor.impl))
++        {
++            // This sensor never was successfully instantiated
++            continue;
++        }
++
++        collectSensorReading(sensor.redfishPath);
++        return;
++    }
++
++    // At this point, nothing more to read, done with this cycle
++    auto now = std::chrono::steady_clock::now();
++    auto msTaken =
++        std::chrono::duration_cast<std::chrono::milliseconds>(now - timeReading)
++            .count();
++
++    // Makes it easy to compute average time per reading cycle
++    msReadingTime += msTaken;
++    ++completionsReading;
++
++    if constexpr (debug)
++    {
++        double average = static_cast<double>(msReadingTime);
++        average /= static_cast<double>(completionsReading);
++        auto msAverage = static_cast<int>(average);
++
++        std::cerr << "Reading done, " << msTaken << " ms, " << msAverage
++                  << " ms average\n";
++    }
++}
+diff --git a/src/RedfishSensorMain.cpp b/src/RedfishSensorMain.cpp
+new file mode 100644
+index 0000000..d46e3a6
+--- /dev/null
++++ b/src/RedfishSensorMain.cpp
+@@ -0,0 +1,628 @@
++#include <RedfishSensor.hpp>
++#include <Utils.hpp>
++#include <boost/asio/steady_timer.hpp>
++#include <boost/container/flat_map.hpp>
++#include <boost/container/flat_set.hpp>
++#include <nlohmann/json.hpp>
++#include <sdbusplus/asio/connection.hpp>
++#include <sdbusplus/asio/object_server.hpp>
++#include <sdbusplus/bus/match.hpp>
++
++#include <string>
++#include <vector>
++
++static constexpr bool debug = false;
++
++static constexpr auto sensorTypes{std::to_array<const char*>(
++    {"RedfishSensor", "RedfishChassis", "RedfishServer"})};
++
++class Globals
++{
++  public:
++    // Common globals for communication to the outside world
++    std::shared_ptr<boost::asio::io_service> ioContext;
++    std::shared_ptr<sdbusplus::asio::connection> systemBus;
++    std::shared_ptr<sdbusplus::asio::object_server> objectServer;
++
++    // Data structures parsed from config
++    boost::container::flat_map<std::string, std::shared_ptr<RedfishServer>>
++        serversConfig;
++    boost::container::flat_map<std::string, std::shared_ptr<RedfishChassis>>
++        chassisesConfig;
++    boost::container::flat_map<std::string, std::shared_ptr<RedfishSensor>>
++        sensorsConfig;
++
++    Globals() :
++        ioContext(std::make_shared<boost::asio::io_service>()),
++        systemBus(std::make_shared<sdbusplus::asio::connection>(*ioContext)),
++        objectServer(
++            std::make_shared<sdbusplus::asio::object_server>(systemBus, true))
++    {
++        objectServer->add_manager("/xyz/openbmc_project/sensors");
++        systemBus->request_name("xyz.openbmc_project.RedfishSensor");
++    }
++
++    Globals(const Globals& copy) = delete;
++    Globals& operator=(const Globals& assign) = delete;
++    ~Globals() = default;
++};
++
++// Validate each sensor has a link to a named Chassis and Server
++void validateCreation(Globals& globals)
++{
++    // Mark all config objects as not relevant, until validated
++    for (auto& serverPair : globals.serversConfig)
++    {
++        serverPair.second->isRelevant = false;
++    }
++    for (auto& chassisPair : globals.chassisesConfig)
++    {
++        chassisPair.second->isRelevant = false;
++    }
++    for (auto& sensorPair : globals.sensorsConfig)
++    {
++        sensorPair.second->isRelevant = false;
++    }
++
++    if constexpr (debug)
++    {
++        std::cerr << "Before validation: " << globals.serversConfig.size()
++                  << " servers, " << globals.chassisesConfig.size()
++                  << " chassises, " << globals.sensorsConfig.size()
++                  << " sensors\n";
++    }
++
++    // Remove, on all servers, links back to sensors
++    for (auto& serverPair : globals.serversConfig)
++    {
++        for (auto& sensorPtr : serverPair.second->sensorsServed)
++        {
++            sensorPtr.reset();
++        }
++        serverPair.second->sensorsServed.clear();
++    }
++
++    // Remove, on all chassises, links back to sensors
++    for (auto& chassisPair : globals.chassisesConfig)
++    {
++        for (auto& sensorPtr : chassisPair.second->sensorsContained)
++        {
++            sensorPtr.reset();
++        }
++        chassisPair.second->sensorsContained.clear();
++    }
++
++    size_t relevantServers = 0;
++    size_t relevantChassises = 0;
++    size_t relevantSensors = 0;
++
++    for (auto& sensorPair : globals.sensorsConfig)
++    {
++        RedfishSensor& sensor = *(sensorPair.second);
++
++        // Remove links going the other way, links out from sensors
++        sensor.server.reset();
++        sensor.chassis.reset();
++
++        auto foundServer = globals.serversConfig.find(sensor.configServer);
++        if (foundServer == globals.serversConfig.end())
++        {
++            std::cerr << "Sensor " << sensor.configName << " has server "
++                      << sensor.configServer << " not found in configuration\n";
++            continue;
++        }
++
++        auto foundChassis = globals.chassisesConfig.find(sensor.configChassis);
++        if (foundChassis == globals.chassisesConfig.end())
++        {
++            std::cerr << "Sensor " << sensor.configName << " has chassis "
++                      << sensor.configChassis
++                      << " not found in configuration\n";
++            continue;
++        }
++
++        // Sensor looks good, repopulate links going out from it
++        sensor.server = foundServer->second;
++        sensor.chassis = foundChassis->second;
++
++        // Correspondingly repopulate links back to this sensor
++        foundServer->second->sensorsServed.emplace_back(sensorPair.second);
++        foundChassis->second->sensorsContained.emplace_back(sensorPair.second);
++
++        // All these objects confirmed relevant to each other
++        ++relevantSensors;
++        foundServer->second->isRelevant = true;
++        foundChassis->second->isRelevant = true;
++        sensor.isRelevant = true;
++    }
++
++    // Show me what you got
++    for (auto& serverPair : globals.serversConfig)
++    {
++        if (serverPair.second->isRelevant)
++        {
++            ++relevantServers;
++            if constexpr (debug)
++            {
++                std::cerr << "Server " << serverPair.first << " has "
++                          << serverPair.second->sensorsServed.size()
++                          << " sensors\n";
++            }
++        }
++        else
++        {
++            std::cerr << "Server " << serverPair.first
++                      << " is not relevant to sensor configuration\n";
++        }
++    }
++
++    for (auto& chassisPair : globals.chassisesConfig)
++    {
++        if (chassisPair.second->isRelevant)
++        {
++            ++relevantChassises;
++            if constexpr (debug)
++            {
++                std::cerr << "Chassis " << chassisPair.first << " has "
++                          << chassisPair.second->sensorsContained.size()
++                          << " sensors\n";
++            }
++        }
++        else
++        {
++            std::cerr << "Chassis " << chassisPair.first
++                      << " is not relevant to sensor configuration\n";
++        }
++    }
++
++    if (relevantSensors > 0)
++    {
++        std::cerr << "Configuration accepted: " << relevantServers
++                  << " servers, " << relevantChassises << " chassises, "
++                  << relevantSensors << " sensors\n";
++    }
++}
++
++bool fillConfigString(const SensorBaseConfigMap& baseConfigMap,
++                      const std::string& interfacePath,
++                      const std::string& paramName, bool isMandatory,
++                      std::string& paramValue)
++{
++    auto paramFound = baseConfigMap.find(paramName);
++    if (paramFound == baseConfigMap.end())
++    {
++        if (isMandatory)
++        {
++            std::cerr << paramName << " parameter not found for "
++                      << interfacePath << "\n";
++            return false;
++        }
++        paramValue.clear();
++        return true;
++    }
++    paramValue = std::visit(VariantToStringVisitor(), paramFound->second);
++    if (paramValue.empty())
++    {
++        std::cerr << paramName << " parameter not parsed for " << interfacePath
++                  << "\n";
++        return false;
++    }
++    return true;
++}
++
++void startServers(Globals& globals)
++{
++    for (const auto& serverPair : globals.serversConfig)
++    {
++        RedfishServer& server = *(serverPair.second);
++
++        if (!(server.isRelevant))
++        {
++            continue;
++        }
++
++        server.provideContext(globals.ioContext, globals.systemBus,
++                              globals.objectServer);
++        server.startNetworking();
++        server.startTimer();
++    }
++}
++
++void createSensorsCallback(
++    Globals& globals, boost::container::flat_set<std::string>& sensorsChanged,
++    const ManagedObjectType& sensorConfigurations)
++{
++    if constexpr (debug)
++    {
++        if (sensorsChanged.empty())
++        {
++            std::cerr << "RedfishSensor creating sensors for the first time\n";
++        }
++        else
++        {
++            std::cerr << "RedfishSensor creating sensors, changed:\n";
++            for (const std::string& s : sensorsChanged)
++            {
++                std::cerr << s << "\n";
++            }
++        }
++    }
++
++    for (const std::pair<sdbusplus::message::object_path, SensorData>& sensor :
++         sensorConfigurations)
++    {
++        const std::string& interfacePath = sensor.first.str;
++        const SensorData& sensorData = sensor.second;
++
++        const SensorBaseConfigMap* baseConfigFound = nullptr;
++        std::string sensorType;
++        for (const char* type : sensorTypes)
++        {
++            auto sensorBase = sensorData.find(configInterfaceName(type));
++            if (sensorBase != sensorData.end())
++            {
++                baseConfigFound = &sensorBase->second;
++                sensorType = type;
++                break;
++            }
++        }
++        if (baseConfigFound == nullptr)
++        {
++            std::cerr << "Base configuration not found for " << interfacePath
++                      << "\n";
++            continue;
++        }
++
++        const SensorBaseConfigMap& baseConfigMap = *baseConfigFound;
++        std::string sensorName;
++
++        // Name is mandatory
++        if (!fillConfigString(baseConfigMap, interfacePath, "Name", true,
++                              sensorName))
++        {
++            continue;
++        }
++
++        if constexpr (debug)
++        {
++            std::cerr << "Found config object: name " << sensorName << ", type "
++                      << sensorType << "\n";
++        }
++
++        // Search for pre-existing name in the appropriate data structure
++        auto findSensor = globals.sensorsConfig.end();
++        auto findChassis = globals.chassisesConfig.end();
++        auto findServer = globals.serversConfig.end();
++        if (sensorType == "RedfishSensor")
++        {
++            findSensor = globals.sensorsConfig.find(sensorName);
++        }
++        if (sensorType == "RedfishChassis")
++        {
++            findChassis = globals.chassisesConfig.find(sensorName);
++        }
++        if (sensorType == "RedfishServer")
++        {
++            findServer = globals.serversConfig.find(sensorName);
++        }
++
++        bool alreadyExisting = false;
++        bool foundChanged = false;
++
++        if ((findSensor != globals.sensorsConfig.end()) ||
++            (findChassis != globals.chassisesConfig.end()) ||
++            (findServer != globals.serversConfig.end()))
++        {
++            alreadyExisting = true;
++        }
++
++        // On rescans, only update sensors we were signaled by
++        std::string suffixName = "/";
++        suffixName += sensor_paths::escapePathForDbus(sensorName);
++        for (const std::string& it : sensorsChanged)
++        {
++            std::string suffixIt = "/";
++            suffixIt += it;
++
++            if (suffixIt.ends_with(suffixName))
++            {
++                if (findSensor != globals.sensorsConfig.end())
++                {
++                    findSensor->second = nullptr;
++                    globals.sensorsConfig.erase(findSensor);
++                }
++                if (findChassis != globals.chassisesConfig.end())
++                {
++                    findChassis->second = nullptr;
++                    globals.chassisesConfig.erase(findChassis);
++                }
++                if (findServer != globals.serversConfig.end())
++                {
++                    findServer->second = nullptr;
++                    globals.serversConfig.erase(findServer);
++                }
++
++                sensorsChanged.erase(it);
++                foundChanged = true;
++                break;
++            }
++        }
++
++        if (alreadyExisting)
++        {
++            if (foundChanged)
++            {
++                std::cerr << "Rebuilding configuration object " << sensorName
++                          << "\n";
++            }
++            else
++            {
++                // Avoid disturbing existing unchanged sensors
++                continue;
++            }
++        }
++        else
++        {
++            if constexpr (debug)
++            {
++                std::cerr << "New configuration object " << sensorName << " by "
++                          << (foundChanged ? "message" : "first pass") << "\n";
++            }
++        }
++
++        // Handle servers separately
++        if (sensorType == "RedfishServer")
++        {
++            std::string serverHost;
++
++            // Host is only mandatory parameter
++            if (!fillConfigString(baseConfigMap, interfacePath, "Host", true,
++                                  serverHost))
++            {
++                continue;
++            }
++            // FUTURE: Parse optional parameters
++
++            auto newServer = std::make_shared<RedfishServer>();
++
++            newServer->configName = sensorName;
++
++            newServer->host = serverHost;
++            // FUTURE: Provide optional parameters
++
++            globals.serversConfig[sensorName] = newServer;
++
++            if constexpr (debug)
++            {
++                std::cerr << "Added server " << sensorName << ": host "
++                          << serverHost << "\n";
++            }
++            continue;
++        }
++
++        // Handle chassises separately
++        if (sensorType == "RedfishChassis")
++        {
++            std::string redName;
++            std::string redId;
++            std::string chaManuf;
++            std::string chaModel;
++            std::string chaPart;
++            std::string chaSku;
++            std::string chaSerial;
++            std::string chaSpare;
++            std::string chaVersion;
++
++            if ((!fillConfigString(baseConfigMap, interfacePath, "RedfishName",
++                                   false, redName)) ||
++                (!fillConfigString(baseConfigMap, interfacePath, "RedfishId",
++                                   false, redId)) ||
++                (!fillConfigString(baseConfigMap, interfacePath, "Manufacturer",
++                                   false, chaManuf)) ||
++                (!fillConfigString(baseConfigMap, interfacePath, "Model", false,
++                                   chaModel)) ||
++                (!fillConfigString(baseConfigMap, interfacePath, "PartNumber",
++                                   false, chaPart)) ||
++                (!fillConfigString(baseConfigMap, interfacePath, "SKU", false,
++                                   chaSku)) ||
++                (!fillConfigString(baseConfigMap, interfacePath, "SerialNumber",
++                                   false, chaSerial)) ||
++                (!fillConfigString(baseConfigMap, interfacePath,
++                                   "SparePartNumber", false, chaSpare)) ||
++                (!fillConfigString(baseConfigMap, interfacePath, "Version",
++                                   false, chaVersion)))
++            {
++                continue;
++            }
++
++            // All parameters are optional, but at least one must be given
++            if (redName.empty() && redId.empty() && chaManuf.empty() &&
++                chaModel.empty() && chaPart.empty() && chaSku.empty() &&
++                chaSerial.empty() && chaSpare.empty() && chaVersion.empty())
++            {
++                std::cerr << "Chassis " << sensorName
++                          << " must have at least one identifying "
++                             "characteristic provided\n";
++                continue;
++            }
++
++            auto newChassis = std::make_shared<RedfishChassis>();
++
++            newChassis->configName = sensorName;
++
++            newChassis->characteristics.redfishName = redName;
++            newChassis->characteristics.redfishId = redId;
++
++            newChassis->characteristics.manufacturer = chaManuf;
++            newChassis->characteristics.model = chaModel;
++            newChassis->characteristics.partNumber = chaPart;
++            newChassis->characteristics.sku = chaSku;
++            newChassis->characteristics.serialNumber = chaSerial;
++            newChassis->characteristics.sparePartNumber = chaSpare;
++            newChassis->characteristics.version = chaVersion;
++
++            globals.chassisesConfig[sensorName] = newChassis;
++
++            if constexpr (debug)
++            {
++                std::cerr << "Added chassis " << sensorName << "\n";
++            }
++            continue;
++        }
++
++        // At this point, the new object is assumed to be a sensor
++        std::string redfishName;
++        std::string redfishId;
++        std::string linkChassis;
++        std::string linkServer;
++
++        PowerState readState = getPowerState(baseConfigMap);
++
++        // The links to desired Chassis and Server are mandatory
++        if ((!fillConfigString(baseConfigMap, interfacePath, "RedfishName",
++                               false, redfishName)) ||
++            (!fillConfigString(baseConfigMap, interfacePath, "RedfishId", false,
++                               redfishId)) ||
++            (!fillConfigString(baseConfigMap, interfacePath, "Chassis", true,
++                               linkChassis)) ||
++            (!fillConfigString(baseConfigMap, interfacePath, "Server", true,
++                               linkServer)))
++        {
++            continue;
++        }
++
++        // RedfishName and RedfishId are optional, but one must be given
++        if (redfishName.empty() && redfishId.empty())
++        {
++            std::cerr << "Sensor " << sensorName
++                      << " must have at least one identifying characteristic "
++                         "provided\n";
++            continue;
++        }
++
++        auto newSensor = std::make_shared<RedfishSensor>();
++
++        newSensor->configName = sensorName;
++        newSensor->inventoryPath = interfacePath;
++
++        newSensor->characteristics.redfishName = redfishName;
++        newSensor->characteristics.redfishId = redfishId;
++
++        newSensor->configChassis = linkChassis;
++        newSensor->configServer = linkServer;
++        newSensor->powerState = readState;
++
++        globals.sensorsConfig[sensorName] = newSensor;
++
++        if constexpr (debug)
++        {
++            std::cerr << "Added sensor " << sensorName << ": chassis "
++                      << linkChassis << ", server " << linkServer << "\n";
++        }
++    }
++
++    // The sensorsChanged list should have been entirely consumed
++    for (const std::string& s : sensorsChanged)
++    {
++        std::cerr << "Sensor was supposed to be changed but not found: " << s
++                  << "\n";
++    }
++    sensorsChanged.clear();
++
++    validateCreation(globals);
++
++    startServers(globals);
++}
++
++void createSensors(Globals& globals,
++                   boost::container::flat_set<std::string>& sensorsChanged)
++{
++    auto getter = std::make_shared<GetSensorConfiguration>(
++        globals.systemBus,
++        [&globals, &sensorsChanged](const ManagedObjectType& sensorConfigs) {
++        createSensorsCallback(globals, sensorsChanged, sensorConfigs);
++        });
++
++    getter->getConfiguration(
++        std::vector<std::string>(sensorTypes.begin(), sensorTypes.end()));
++}
++
++int main()
++{
++    if constexpr (debug)
++    {
++        std::cerr << "RedfishSensor: Service starting up\n";
++    }
++
++    Globals globals;
++
++    // Not just sensors changed, Chassis and Server could also be changed
++    boost::container::flat_set<std::string> sensorsChanged;
++
++    globals.ioContext->post([&globals, &sensorsChanged]() mutable {
++        createSensors(globals, sensorsChanged);
++    });
++
++    boost::asio::steady_timer filterTimer(*(globals.ioContext));
++    std::function<void(sdbusplus::message_t&)> eventHandler =
++        [&filterTimer, &globals,
++         &sensorsChanged](sdbusplus::message_t& message) mutable {
++        if (message.is_method_error())
++        {
++            std::cerr << "Callback method error in main\n";
++            return;
++        }
++
++        const std::string messagePath = message.get_path();
++        sensorsChanged.insert(messagePath);
++        if constexpr (debug)
++        {
++            std::cerr << "Received message from " << messagePath;
++        }
++
++        // Defer action, so rapidly incoming requests can be batched up
++        filterTimer.expires_from_now(std::chrono::seconds(1));
++
++        filterTimer.async_wait(
++            [&globals,
++             &sensorsChanged](const boost::system::error_code& ec) mutable {
++            if (ec == boost::asio::error::operation_aborted)
++            {
++                if constexpr (debug)
++                {
++                    std::cerr << "Timer cancelled\n";
++                }
++                return;
++            }
++            if (ec)
++            {
++                std::cerr << "Timer error: " << ec.message() << "\n";
++                return;
++            }
++
++            if constexpr (debug)
++            {
++                std::cerr << "Now changing sensors\n";
++            }
++            createSensors(globals, sensorsChanged);
++        });
++    };
++
++    std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
++        setupPropertiesChangedMatches(*(globals.systemBus), sensorTypes,
++                                      eventHandler);
++
++    if constexpr (debug)
++    {
++        std::cerr << "RedfishSensor: Service entering main loop\n";
++    }
++
++    globals.ioContext->run();
++
++    if constexpr (debug)
++    {
++        std::cerr << "RedfishSensor: Service shutting down\n";
++    }
++
++    return 0;
++}
+diff --git a/src/RedfishSensorQuery.cpp b/src/RedfishSensorQuery.cpp
+new file mode 100644
+index 0000000..6b8f873
+--- /dev/null
++++ b/src/RedfishSensorQuery.cpp
+@@ -0,0 +1,915 @@
++#include <RedfishSensor.hpp>
++#include <boost/asio/io_service.hpp>
++#include <boost/asio/ip/tcp.hpp>
++#include <boost/asio/steady_timer.hpp>
++#include <boost/asio/strand.hpp>
++#include <boost/beast/core.hpp>
++#include <boost/beast/http.hpp>
++
++static constexpr bool debug = false;
++
++// Dumps all content received, dangerous (no escaping), only if debug true
++static constexpr bool dumpContent = false;
++
++// FUTURE: This parameter could come in from configuration
++static constexpr double networkTimeout = 30;
++
++// Arbitrary limit on network request and reply size, in bytes
++static constexpr int sizeLimit = 1024 * 1024;
++
++// This one goes up to 11
++static constexpr int httpVersion = 11;
++
++// FUTURE: Change to std::string type when allowed by compiler
++static constexpr auto userAgent = "RedfishSensor/1.0";
++static constexpr auto redfishRoot = "/redfish/v1";
++
++// This class acts as an oracle, taking a string from caller, and eventually
++// calling the callback, supplying another string as an answer. The caller
++// does not need to know any details of what goes on behind the scenes.
++class RedfishConnection : public std::enable_shared_from_this<RedfishConnection>
++{
++  public:
++    RedfishConnection(const std::shared_ptr<boost::asio::io_service>& io,
++                      const std::string& host, const std::string& protocol) :
++        ioContext(io),
++        mockTimer(*io), netResolver(boost::asio::make_strand(*io)),
++        netStream(boost::asio::make_strand(*io)), serverHost(host),
++        serverProtocol(protocol)
++    {
++        // No body necessary
++    }
++
++    // Call this before submitTransaction to mock that transaction
++    void enableMock(int msDelay, int mockErrorCode,
++                    const std::string& mockResponse);
++
++    void submitTransaction(const std::string& request,
++                           const std::function<void(const std::string& response,
++                                                    int code)>& callback);
++
++  private:
++    bool mockEnabled = false;
++    int amockTime = 0;
++    int mockError = 0;
++    std::string mockReply;
++
++    bool isResolved = false;
++    bool isConnected = false;
++
++    std::shared_ptr<boost::asio::io_service> ioContext;
++    boost::asio::steady_timer mockTimer;
++
++    // Stuff to feed the Beast with
++    boost::asio::ip::tcp::resolver netResolver;
++    boost::beast::tcp_stream netStream;
++    boost::beast::flat_buffer netBuffer;
++    boost::beast::http::request<boost::beast::http::empty_body> httpReq;
++    boost::beast::http::response<boost::beast::http::string_body> httpResp;
++    std::optional<
++        boost::beast::http::request_serializer<boost::beast::http::empty_body>>
++        reqSerializer;
++    std::optional<
++        boost::beast::http::response_parser<boost::beast::http::string_body>>
++        respParser;
++
++    // For resolution
++    // FUTURE: Add support for SSL (if https), TCP port, user, password
++    std::string serverHost;
++    std::string serverProtocol;
++    boost::asio::ip::tcp::endpoint serverEndpoint;
++
++    uint64_t bytesRead = 0;
++    uint64_t bytesWrite = 0;
++
++    int transactionsAttempted = 0;
++    int transactionsCompleted = 0;
++
++    void mockTransaction(const std::string& request,
++                         const std::function<void(const std::string& response,
++                                                  int code)>& callback);
++
++    void submitResolution(const std::string& request,
++                          const std::function<void(const std::string& response,
++                                                   int code)>& callback);
++    void submitConnection(const std::string& request,
++                          const std::function<void(const std::string& response,
++                                                   int code)>& callback);
++    void submitQuery(const std::string& request,
++                     const std::function<void(const std::string& response,
++                                              int code)>& callback);
++    void submitReply(const std::string& request,
++                     const std::function<void(const std::string& response,
++                                              int code)>& callback);
++
++    void handleResolution(
++        const std::string& request,
++        const std::function<void(const std::string& response, int code)>&
++            callback,
++        const boost::system::error_code& ec,
++        const boost::asio::ip::tcp::resolver::results_type& results);
++};
++
++void RedfishConnection::enableMock(int msDelay, int mockErrorCode,
++                                   const std::string& mockResponse)
++{
++    mockEnabled = true;
++    amockTime = msDelay;
++    mockError = mockErrorCode;
++    mockReply = mockResponse;
++}
++
++void RedfishConnection::mockTransaction(
++    const std::string& request,
++    const std::function<void(const std::string& response, int code)>& callback)
++{
++    auto weakThis = weak_from_this();
++
++    // Each mock is a one-shot
++    mockEnabled = false;
++
++    mockTimer.expires_after(std::chrono::milliseconds(amockTime));
++
++    mockTimer.async_wait(
++        [weakThis, callback](const boost::system::error_code& ec) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            std::cerr << "Timer callback error: Object disappeared\n";
++            return;
++        }
++        if (ec)
++        {
++            std::cerr << "Timer callback error: " << ec.message();
++            return;
++        }
++
++        callback(lockThis->mockReply, lockThis->mockError);
++
++        if constexpr (debug)
++        {
++            std::cerr << "Mock transaction completed: "
++                      << lockThis->mockReply.size() << " bytes, "
++                      << lockThis->mockError << " code, " << lockThis->amockTime
++                      << " ms\n";
++            if constexpr (dumpContent)
++            {
++                std::cerr << lockThis->mockReply << "\n";
++            }
++        }
++    });
++
++    if constexpr (debug)
++    {
++        std::cerr << "Mock transaction submitted: " << request << "\n";
++    }
++}
++
++void RedfishConnection::submitTransaction(
++    const std::string& request,
++    const std::function<void(const std::string& response, int code)>& callback)
++{
++    ++transactionsAttempted;
++
++    // Take the easy way out, if we can
++    if (mockEnabled)
++    {
++        mockTransaction(request, callback);
++        return;
++    }
++
++    // Callbacks will chain: Resolve -> Connect -> Query -> Reply
++    if (!isResolved)
++    {
++        submitResolution(request, callback);
++        return;
++    }
++
++    // Callbacks will chain: Connect -> Query -> Reply
++    if (!isConnected)
++    {
++        submitConnection(request, callback);
++        return;
++    }
++
++    // Things look good from before, fastest path: Query -> Reply
++    submitQuery(request, callback);
++}
++
++// Handles both the blocking and async versions of the resolver
++void RedfishConnection::handleResolution(
++    const std::string& request,
++    const std::function<void(const std::string& response, int code)>& callback,
++    const boost::system::error_code& ec,
++    const boost::asio::ip::tcp::resolver::results_type& results)
++{
++    isResolved = false;
++
++    if (ec)
++    {
++        std::cerr << "Network resolution failure: " << ec.message() << "\n";
++
++        // Give the user the bad news
++        callback("", ec.value());
++        return;
++    }
++
++    for (const auto& result : results)
++    {
++        if constexpr (debug)
++        {
++            std::cerr << "Network resolution endpoint: " << result.endpoint()
++                      << "\n";
++        }
++
++        // Accept the first endpoint in the results list
++        // FUTURE: Perhaps handle multiple endpoints in the future
++        if (!isResolved)
++        {
++            isResolved = true;
++            serverEndpoint = result.endpoint();
++        }
++    }
++
++    if (!isResolved)
++    {
++        std::cerr << "Network resolution failure: No results\n";
++
++        // Give the user the bad news
++        callback("", ec.value());
++        return;
++    }
++
++    // After resolution, proceed to connection
++    submitConnection(request, callback);
++
++    if constexpr (debug)
++    {
++        std::cerr << "Network resolution success: host " << serverHost
++                  << ", endpoint " << serverEndpoint << "\n";
++    }
++}
++
++// FUTURE: Allow https and/or custom port number
++void RedfishConnection::submitResolution(
++    const std::string& request,
++    const std::function<void(const std::string& response, int code)>& callback)
++{
++    // Boost limitation, async_resolve() forcefully creates a new thread
++    // Workaround for the resulting exception abort if threads disabled
++#ifdef BOOST_ASIO_DISABLE_THREADS
++    if constexpr (debug)
++    {
++        std::cerr << "Network attempting blocking resolution: host "
++                  << serverHost << ", protocol " << serverProtocol << "\n";
++    }
++
++    boost::system::error_code ec;
++    boost::asio::ip::tcp::resolver::results_type results =
++        netResolver.resolve(serverHost, serverProtocol, ec);
++    handleResolution(request, callback, ec, results);
++
++    return;
++#endif
++
++    auto weakThis = weak_from_this();
++
++    // FUTURE: Use another timer, because resolver does not have a timeout
++    netResolver.async_resolve(
++        serverHost, serverProtocol,
++        [weakThis, request, callback](
++            const boost::system::error_code& ec,
++            const boost::asio::ip::tcp::resolver::results_type& results) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            std::cerr << "Network resolution failure: Object has disappeared\n";
++            return;
++        }
++        lockThis->handleResolution(request, callback, ec, results);
++        });
++
++    if constexpr (debug)
++    {
++        std::cerr << "Network attempting threaded resolution: host "
++                  << serverHost << ", protocol " << serverProtocol << "\n";
++    }
++}
++
++void RedfishConnection::submitConnection(
++    const std::string& request,
++    const std::function<void(const std::string& response, int code)>& callback)
++{
++    auto weakThis = weak_from_this();
++
++    netStream.expires_after(
++        std::chrono::duration_cast<std::chrono::steady_clock::duration>(
++            std::chrono::duration<double>(networkTimeout)));
++
++    netStream.async_connect(serverEndpoint, [weakThis, request, callback](
++                                                boost::beast::error_code ec) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            std::cerr << "Network connection failure: Object has disappeared\n";
++            return;
++        }
++
++        if (ec)
++        {
++            std::cerr << "Network connection failure: " << ec.message() << "\n";
++
++            // FUTURE: Back all the way out, do resolve again if connect fails
++            lockThis->isConnected = false;
++
++            // Give the user the bad news
++            callback("", ec.value());
++            return;
++        }
++
++        // Good connection
++        lockThis->isConnected = true;
++
++        // After connection, proceed to query
++        lockThis->submitQuery(request, callback);
++
++        if constexpr (debug)
++        {
++            std::cerr << "Network connection success\n";
++        }
++    });
++
++    if constexpr (debug)
++    {
++        std::cerr << "Network attempting connection: " << serverEndpoint
++                  << "\n";
++    }
++}
++
++void RedfishConnection::submitQuery(
++    const std::string& request,
++    const std::function<void(const std::string& response, int code)>& callback)
++{
++    auto weakThis = weak_from_this();
++
++    // FUTURE: Support HTTP username/password, if given, and SSL
++    httpReq.method(boost::beast::http::verb::get);
++    httpReq.target(request);
++    httpReq.version(httpVersion);
++    httpReq.keep_alive(true);
++    httpReq.set(boost::beast::http::field::host, serverHost);
++    httpReq.set(boost::beast::http::field::user_agent, userAgent);
++
++    // The parser must live through async, but reconstructed before each message
++    reqSerializer.reset();
++    reqSerializer.emplace(httpReq);
++
++    reqSerializer->limit(sizeLimit);
++
++    netStream.expires_after(
++        std::chrono::duration_cast<std::chrono::steady_clock::duration>(
++            std::chrono::duration<double>(networkTimeout)));
++
++    boost::beast::http::async_write(
++        netStream, *reqSerializer,
++        [weakThis, request, callback](const boost::beast::error_code ec,
++                                      std::size_t bytesTransferred) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            std::cerr << "Network query failure: Object has disappeared\n";
++            return;
++        }
++
++        if (ec)
++        {
++            std::cerr << "Network query failure: " << ec.message() << "\n";
++
++            // Connection is unusable after error, tear it down
++            lockThis->netStream.close();
++            lockThis->isConnected = false;
++
++            // Give the user the bad news
++            callback("", ec.value());
++            return;
++        }
++
++        lockThis->bytesWrite += bytesTransferred;
++
++        // After query, proceed to reply
++        lockThis->submitReply(request, callback);
++
++        if constexpr (debug)
++        {
++            std::cerr << "Network query success: " << bytesTransferred
++                      << " bytes transferred\n";
++        }
++        });
++
++    if constexpr (debug)
++    {
++        std::cerr << "Network attempting query: " << request << "\n";
++    }
++}
++
++void RedfishConnection::submitReply(
++    const std::string& request,
++    const std::function<void(const std::string& response, int code)>& callback)
++{
++    auto weakThis = weak_from_this();
++
++    // The parser must live through async, but reconstructed before each message
++    respParser.reset();
++    respParser.emplace();
++
++    respParser->header_limit(sizeLimit);
++    respParser->body_limit(sizeLimit);
++
++    netStream.expires_after(
++        std::chrono::duration_cast<std::chrono::steady_clock::duration>(
++            std::chrono::duration<double>(networkTimeout)));
++
++    boost::beast::http::async_read(
++        netStream, netBuffer, *respParser,
++        [weakThis, request, callback](const boost::beast::error_code ec,
++                                      std::size_t bytesTransferred) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            std::cerr << "Network reply failure: Object has disappeared\n";
++            return;
++        }
++
++        if (ec)
++        {
++            std::cerr << "Network reply failure: " << ec.message() << "\n";
++
++            // Connection is unusable after error, tear it down
++            lockThis->netStream.close();
++            lockThis->isConnected = false;
++
++            // Give the user the bad news
++            callback("", ec.value());
++            return;
++        }
++
++        lockThis->httpResp = lockThis->respParser->release();
++
++        lockThis->bytesRead += bytesTransferred;
++        ++(lockThis->transactionsCompleted);
++
++        int resultCode = lockThis->httpResp.result_int();
++        if (lockThis->httpResp.result() != boost::beast::http::status::ok)
++        {
++            // No return here, this is only a warning, not an error
++            std::cerr << "Network reply code: " << resultCode << " "
++                      << lockThis->httpResp.result() << "\n";
++        }
++
++        // Finally some good news
++        callback(lockThis->httpResp.body(), resultCode);
++
++        if constexpr (debug)
++        {
++            std::cerr << "Network reply success: " << bytesTransferred
++                      << " bytes transferred\n";
++        }
++        });
++
++    if constexpr (debug)
++    {
++        std::cerr << "Network attempting reply\n";
++    }
++}
++
++void RedfishTransaction::handleCallback(const std::string& response, int code)
++{
++    std::chrono::steady_clock::time_point now =
++        std::chrono::steady_clock::now();
++
++    auto msSince =
++        std::chrono::duration_cast<std::chrono::milliseconds>(now - timeStarted)
++            .count();
++
++    if constexpr (debug)
++    {
++        std::cerr << "REPLY: " << response.size() << " bytes, " << code
++                  << " code, " << msSince << " ms\n";
++
++        if constexpr (dumpContent)
++        {
++            std::cerr << response << "\n";
++        }
++    }
++
++    RedfishCompletion reply;
++
++    reply.queryRequest = queryRequest;
++    reply.queryResponse = response;
++    reply.msElapsed = msSince;
++    reply.errorCode = code;
++
++    // Turn response string into JSON early, saving caller some work
++    // Pass 3rd argument to avoid throwing exceptions
++    reply.jsonResponse = nlohmann::json::parse(response, nullptr, false);
++    if (!(reply.jsonResponse.is_object()))
++    {
++        // No return here, this is a warning, not a fatal error
++        std::cerr << "Transaction returned content that is not a JSON object\n";
++    }
++
++    // Tell the caller what just happened
++    callbackResponse(reply);
++
++    if constexpr (debug)
++    {
++        std::cerr << "Reply callback called\n";
++    }
++}
++
++void RedfishTransaction::submitRequest(
++    const std::shared_ptr<RedfishConnection>& connection)
++{
++    if constexpr (debug)
++    {
++        std::cerr << "QUERY: " << queryRequest << "\n";
++    }
++
++    auto weakThis = weak_from_this();
++
++    timeStarted = std::chrono::steady_clock::now();
++
++    connection->submitTransaction(
++        queryRequest, [weakThis](const std::string& response, int code) {
++            auto lockThis = weakThis.lock();
++            if (!lockThis)
++            {
++                std::cerr
++                    << "Transaction callback ignored: Object has disappeared\n";
++                return;
++            }
++
++            lockThis->handleCallback(response, code);
++        });
++
++    if constexpr (debug)
++    {
++        std::cerr << "Query submitted\n";
++    }
++}
++
++void RedfishServer::startNetworking()
++{
++    if (networkConnection)
++    {
++        std::cerr << "Internal error: Networking already started\n";
++        return;
++    }
++
++    if (host.empty())
++    {
++        std::cerr << "Internal error: Networking host is empty\n";
++        return;
++    }
++
++    // FUTURE: Allow https (SSL), custom TCP port, HTTP user/password
++    if (protocol.empty())
++    {
++        protocol = "http";
++    }
++
++    networkConnection =
++        make_shared<RedfishConnection>(ioContext, host, protocol);
++}
++
++// To keep the flags and audits correct, wrap your transactions in these
++bool RedfishServer::sendTransaction(const RedfishTransaction& transaction)
++{
++    if (networkBusy)
++    {
++        std::cerr << "Internal error: Overlapping transactions attempted\n";
++        return false;
++    }
++
++    if (!networkConnection)
++    {
++        std::cerr << "Internal error: Networking not successfully started\n";
++        return false;
++    }
++
++    ++transactionsStarted;
++
++    networkBusy = true;
++
++    // Retain ownership of transaction throughout the async
++    activeTransaction.reset();
++    activeTransaction = std::make_shared<RedfishTransaction>(transaction);
++
++    activeTransaction->submitRequest(networkConnection);
++
++    if constexpr (debug)
++    {
++        std::cerr << "Sent transaction: " << activeTransaction->queryRequest
++                  << "\n";
++    }
++
++    return true;
++}
++
++bool RedfishServer::doneTransaction(const RedfishCompletion& completion)
++{
++    networkBusy = false;
++
++    // Transaction is no longer active
++    activeTransaction.reset();
++
++    bool success = false;
++
++    // Success is defined by having a valid JSON object and HTTP success
++    if (completion.jsonResponse.is_object())
++    {
++        if (boost::beast::http::int_to_status(completion.errorCode) ==
++            boost::beast::http::status::ok)
++        {
++            success = true;
++        }
++    }
++
++    msNetworkTime += completion.msElapsed;
++
++    if (success)
++    {
++        ++transactionsSuccess;
++    }
++    else
++    {
++        ++transactionsFailure;
++    }
++
++    if constexpr (debug)
++    {
++        std::cerr << "Done transaction: " << (success ? "Success" : "Failure")
++                  << ", " << completion.msElapsed << " ms\n";
++    }
++
++    return success;
++}
++
++// FUTURE: These query functions are repetitive, consider a template
++void RedfishServer::queryRoot()
++{
++    auto weakThis = weak_from_this();
++
++    RedfishTransaction trans;
++
++    trans.queryRequest = redfishRoot;
++    trans.callbackResponse = [weakThis](const RedfishCompletion& completion) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            return;
++        }
++        if (lockThis->doneTransaction(completion))
++        {
++            if (lockThis->fillFromRoot(completion.jsonResponse))
++            {
++                lockThis->advanceDiscovery();
++            }
++        }
++    };
++
++    sendTransaction(trans);
++}
++
++void RedfishServer::queryTelemetry()
++{
++    auto weakThis = weak_from_this();
++
++    RedfishTransaction trans;
++
++    trans.queryRequest = pathTelemetry;
++    trans.callbackResponse = [weakThis](const RedfishCompletion& completion) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            return;
++        }
++        if (lockThis->doneTransaction(completion))
++        {
++            if (lockThis->fillFromTelemetry(completion.jsonResponse))
++            {
++                lockThis->advanceDiscovery();
++            }
++        }
++    };
++
++    sendTransaction(trans);
++}
++
++void RedfishServer::queryMetricCollection()
++{
++    auto weakThis = weak_from_this();
++
++    RedfishTransaction trans;
++
++    trans.queryRequest = pathMetrics;
++    trans.callbackResponse = [weakThis](const RedfishCompletion& completion) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            return;
++        }
++        if (lockThis->doneTransaction(completion))
++        {
++            if (lockThis->fillFromMetricCollection(completion.jsonResponse))
++            {
++                lockThis->advanceDiscovery();
++            }
++        }
++    };
++
++    sendTransaction(trans);
++}
++
++void RedfishServer::queryMetricReport(const std::string& path)
++{
++    auto weakThis = weak_from_this();
++
++    RedfishTransaction trans;
++
++    trans.queryRequest = path;
++    trans.callbackResponse = [weakThis](const RedfishCompletion& completion) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            return;
++        }
++        if (lockThis->doneTransaction(completion))
++        {
++            if (lockThis->fillFromMetricReport(completion.jsonResponse))
++            {
++                lockThis->advanceDiscovery();
++            }
++        }
++    };
++
++    sendTransaction(trans);
++}
++
++void RedfishServer::queryChassisCollection()
++{
++    auto weakThis = weak_from_this();
++
++    RedfishTransaction trans;
++
++    trans.queryRequest = pathChassis;
++    trans.callbackResponse = [weakThis](const RedfishCompletion& completion) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            return;
++        }
++        if (lockThis->doneTransaction(completion))
++        {
++            if (lockThis->fillFromChassisCollection(completion.jsonResponse))
++            {
++                lockThis->advanceDiscovery();
++            }
++        }
++    };
++
++    sendTransaction(trans);
++}
++
++void RedfishServer::queryChassisCandidate(const std::string& path)
++{
++    auto weakThis = weak_from_this();
++
++    RedfishTransaction trans;
++
++    trans.queryRequest = path;
++    trans.callbackResponse =
++        [weakThis, path](const RedfishCompletion& completion) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            return;
++        }
++        if (lockThis->doneTransaction(completion))
++        {
++            if (lockThis->fillFromChassisCandidate(completion.jsonResponse,
++                                                   path))
++            {
++                lockThis->advanceDiscovery();
++            }
++        }
++    };
++
++    sendTransaction(trans);
++}
++
++void RedfishServer::querySensorCollection(const std::string& path)
++{
++    auto weakThis = weak_from_this();
++
++    RedfishTransaction trans;
++
++    trans.queryRequest = path;
++    trans.callbackResponse =
++        [weakThis, path](const RedfishCompletion& completion) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            return;
++        }
++        if (lockThis->doneTransaction(completion))
++        {
++            if (lockThis->fillFromSensorCollection(completion.jsonResponse,
++                                                   path))
++            {
++                lockThis->advanceDiscovery();
++            }
++        }
++    };
++
++    sendTransaction(trans);
++}
++
++void RedfishServer::querySensorCandidate(const std::string& path)
++{
++    auto weakThis = weak_from_this();
++
++    RedfishTransaction trans;
++
++    trans.queryRequest = path;
++    trans.callbackResponse =
++        [weakThis, path](const RedfishCompletion& completion) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            return;
++        }
++        if (lockThis->doneTransaction(completion))
++        {
++            if (lockThis->fillFromSensorCandidate(completion.jsonResponse,
++                                                  path))
++            {
++                lockThis->advanceDiscovery();
++            }
++        }
++    };
++
++    sendTransaction(trans);
++}
++
++void RedfishServer::collectMetricReport(const std::string& path)
++{
++    auto weakThis = weak_from_this();
++
++    RedfishTransaction trans;
++
++    trans.queryRequest = path;
++    trans.callbackResponse = [weakThis](const RedfishCompletion& completion) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            return;
++        }
++        if (lockThis->doneTransaction(completion))
++        {
++            if (lockThis->acceptMetricReport(completion.jsonResponse,
++                                             lockThis->timeReading))
++            {
++                lockThis->advanceReading();
++            }
++        }
++    };
++
++    sendTransaction(trans);
++}
++
++void RedfishServer::collectSensorReading(const std::string& path)
++{
++    auto weakThis = weak_from_this();
++
++    RedfishTransaction trans;
++
++    trans.queryRequest = path;
++    trans.callbackResponse = [weakThis](const RedfishCompletion& completion) {
++        auto lockThis = weakThis.lock();
++        if (!lockThis)
++        {
++            return;
++        }
++        if (lockThis->doneTransaction(completion))
++        {
++            if (lockThis->acceptSensorReading(completion.jsonResponse,
++                                              lockThis->timeReading))
++            {
++                lockThis->advanceReading();
++            }
++        }
++    };
++
++    sendTransaction(trans);
++}
+diff --git a/src/RedfishSensorResponse.cpp b/src/RedfishSensorResponse.cpp
+new file mode 100644
+index 0000000..008c890
+--- /dev/null
++++ b/src/RedfishSensorResponse.cpp
+@@ -0,0 +1,1252 @@
++#include <RedfishSensor.hpp>
++#include <nlohmann/json.hpp>
++
++#include <chrono>
++#include <string>
++#include <vector>
++
++static constexpr bool debug = false;
++
++// FUTURE: Use std::vector when compiler allows constexpr
++static constexpr std::array<RedfishUnit, 9> unitTable{
++    {// The min and max are arbitrary, used only if Redfish info absent
++     // All these divide by 255.0 into nice IPMI-friendly increments
++     {"Altitude", "Pa", "Pascals", 0, 127500},
++     {"Current", "A", "Amperes", 0, 51},
++     {"EnergyJoules", "J", "Joules", 0, 5100000},
++     {"Percent", "%", "Percent", 0, 127.5},
++     {"Power", "W", "Watts", 0, 5100},
++     {"Rotational", "RPM", "RPMS", 0, 51000},
++     {"Temperature", "Cel", "DegreesC", -128, 127},
++     {"Voltage", "V", "Volts", 0, 510},
++     {"", "", "", 0, 0}}};
++
++// FUTURE: Use std::vector when compiler allows constexpr
++static constexpr std::array<RedfishThreshold, 7> thresholdTable{
++    {// As there is no Redfish equivalent to D-Bus PERFORMANCELOSS
++     // or HARDSHUTDOWN levels, they intentionally do not appear here
++     {"UpperCaution", thresholds::Level::WARNING, thresholds::Direction::HIGH},
++     {"LowerCaution", thresholds::Level::WARNING, thresholds::Direction::LOW},
++     {"UpperCritical", thresholds::Level::CRITICAL,
++      thresholds::Direction::HIGH},
++     {"LowerCritical", thresholds::Level::CRITICAL, thresholds::Direction::LOW},
++     {"UpperFatal", thresholds::Level::SOFTSHUTDOWN,
++      thresholds::Direction::HIGH},
++     {"LowerFatal", thresholds::Level::SOFTSHUTDOWN,
++      thresholds::Direction::LOW},
++     {"", thresholds::Level::ERROR, thresholds::Direction::ERROR}}};
++
++static constexpr RedfishUnitLookup unitLookup{unitTable};
++
++static constexpr RedfishThresholdLookup thresholdLookup{thresholdTable};
++
++static std::string fillFromJsonString(const std::string& name,
++                                      const nlohmann::json& json)
++{
++    std::string empty;
++
++    // Must be an object
++    if (!(json.is_object()))
++    {
++        return empty;
++    }
++
++    // The given field name must be contained within the object
++    auto iter = json.find(name);
++    if (iter == json.end())
++    {
++        return empty;
++    }
++
++    // The field value must be a string
++    if (!(iter->is_string()))
++    {
++        return empty;
++    }
++
++    std::string result = *iter;
++
++    if constexpr (debug)
++    {
++        std::cerr << "Parsed string: name " << name << ", value " << result
++                  << "\n";
++    }
++
++    return result;
++}
++
++static std::string fillFromJsonPath(const std::string& name,
++                                    const nlohmann::json& json)
++{
++    std::string empty;
++
++    // Must be an object
++    if (!(json.is_object()))
++    {
++        return empty;
++    }
++
++    // The given field name must be contained within the object
++    auto iter = json.find(name);
++    if (iter == json.end())
++    {
++        return empty;
++    }
++
++    // The field value must be an object
++    if (!(iter->is_object()))
++    {
++        return empty;
++    }
++
++    // The object must contain this hardcoded field name
++    auto subiter = iter->find("@odata.id");
++    if (subiter == iter->end())
++    {
++        return empty;
++    }
++
++    // The field value must be a string
++    if (!(subiter->is_string()))
++    {
++        return empty;
++    }
++
++    std::string result = *subiter;
++
++    if constexpr (debug)
++    {
++        std::cerr << "Parsed path: name " << name << ", value " << result
++                  << "\n";
++    }
++
++    return result;
++}
++
++// Can have successful NaN returns, such as for a literal string "nan"
++static double stringToNumber(const std::string& text, bool& outSuccessful)
++{
++    outSuccessful = false;
++
++    double result = std::numeric_limits<double>::quiet_NaN();
++
++    // FUTURE: There should be a non-throwing equivalent of std::stod
++    try
++    {
++        result = std::stod(text);
++
++        outSuccessful = true;
++    }
++    catch (const std::exception& e)
++    {
++        result = std::numeric_limits<double>::quiet_NaN();
++
++        std::cerr << "Problem converting string to number: " << e.what()
++                  << "\n";
++    }
++
++    if constexpr (debug)
++    {
++        std::cerr << "Converted string " << text << " to number " << result
++                  << "\n";
++    }
++
++    return result;
++}
++
++static double fillFromJsonNumber(const std::string& name,
++                                 const nlohmann::json& json,
++                                 bool& outSuccessful)
++{
++    outSuccessful = false;
++
++    double nan = std::numeric_limits<double>::quiet_NaN();
++
++    // Must be an object
++    if (!(json.is_object()))
++    {
++        return nan;
++    }
++
++    // The given field name must be contained within the object
++    auto iter = json.find(name);
++    if (iter == json.end())
++    {
++        return nan;
++    }
++
++    // JSON allows numbers to be "null" to indicate not available
++    if (iter->is_null())
++    {
++        // This is actually considered a successful return
++        outSuccessful = true;
++
++        // FUTURE: Add more audits, and chalk this successful, not failure
++        return nan;
++    }
++
++    double result = nan;
++
++    // Some Redfish servers wrap their numbers in strings
++    if (iter->is_string())
++    {
++        bool successful = false;
++
++        result = stringToNumber(*iter, successful);
++
++        if (!successful)
++        {
++            // The string conversion was unsuccessful, ignore result
++            return nan;
++        }
++    }
++    else
++    {
++        if (!(iter->is_number()))
++        {
++            // Unsuccessful result, unrecognized type
++            return result;
++        }
++
++        // The type is already a number, needs no further conversion
++        result = *iter;
++    }
++
++    // Collapse all other weird floating-point (infinity, etc.) into NaN
++    if (!(std::isfinite(result)))
++    {
++        result = nan;
++    }
++
++    if constexpr (debug)
++    {
++        std::cerr << "Parsed number: name " << name << ", value " << result
++                  << "\n";
++    }
++
++    // Returning NaN is still successful if it came from the Redfish server
++    outSuccessful = true;
++
++    return result;
++}
++
++static std::vector<std::string> fillFromJsonMembers(const nlohmann::json& json,
++                                                    bool& outSuccessful)
++{
++    std::vector<std::string> empty;
++
++    outSuccessful = false;
++
++    // Must be an object
++    if (!(json.is_object()))
++    {
++        return empty;
++    }
++
++    // The object must contain this hardcoded field name
++    auto iter = json.find("Members");
++    if (iter == json.end())
++    {
++        return empty;
++    }
++
++    // The field value must be an array
++    if (!(iter->is_array()))
++    {
++        return empty;
++    }
++
++    // FUTURE: For an additional sanity check, we could grab
++    // Members@odata.count here, and compare it with the actual
++    // count, to catch malformed Redfish replies.
++    size_t count = iter->size();
++
++    std::vector<std::string> result;
++
++    for (size_t i = 0; i < count; ++i)
++    {
++        // The element must be an object
++        if (!((*iter)[i].is_object()))
++        {
++            return empty;
++        }
++
++        // The object must contain this hardcoded field name
++        auto subiter = (*iter)[i].find("@odata.id");
++        if (subiter == (*iter)[i].end())
++        {
++            return empty;
++        }
++
++        // The field value must be a string
++        if (!(subiter->is_string()))
++        {
++            return empty;
++        }
++
++        std::string value = *subiter;
++
++        // The field value must not be empty
++        if (value.empty())
++        {
++            return empty;
++        }
++
++        result.emplace_back(value);
++
++        if constexpr (debug)
++        {
++            std::cerr << "Parsed array[" << i << "]: value " << value << "\n";
++        }
++    }
++
++    // Needed to indicate successful return of a collection of zero elements
++    outSuccessful = true;
++
++    return result;
++}
++
++static std::vector<thresholds::Threshold>
++    parseThresholds(const nlohmann::json& json, bool& outSuccessful)
++{
++    std::vector<thresholds::Threshold> empty;
++    std::vector<thresholds::Threshold> results;
++
++    outSuccessful = false;
++
++    // It must be an object
++    if (!(json.is_object()))
++    {
++        return empty;
++    }
++
++    for (const auto& [key, val] : json.items())
++    {
++        // Value must be a JSON dictionary
++        if (!(val.is_object()))
++        {
++            std::cerr << "Skipping over threshold with unparseable value: "
++                      << key << "\n";
++            continue;
++        }
++
++        bool successful = false;
++        double thresholdValue = fillFromJsonNumber("Reading", val, successful);
++
++        if (!successful)
++        {
++            std::cerr << "Skipping over threshold with unrecognizable value: "
++                      << key << "\n";
++            continue;
++        }
++
++        // Not fatal error, some Redfish servers serve "null" values
++        if (!(std::isfinite(thresholdValue)))
++        {
++            std::cerr << "Skipping over threshold with unusable value: " << key
++                      << "\n";
++            continue;
++        }
++
++        // Not fatal error, skip over unrecognized field names
++        std::string nameCheck = thresholdLookup.lookup(key).redfishName;
++        if (nameCheck.empty())
++        {
++            std::cerr << "Skipping over threshold with unrecognizable name: "
++                      << key << "\n";
++            continue;
++        }
++
++        // Name preflight is good, now do the real lookups
++        thresholds::Level dbusLevel = thresholdLookup.lookup(key).level;
++        thresholds::Direction dbusDirection =
++            thresholdLookup.lookup(key).direction;
++
++        results.emplace_back(dbusLevel, dbusDirection, thresholdValue);
++
++        if constexpr (debug)
++        {
++            std::cerr << "Added threshold: type " << nameCheck << ", value "
++                      << thresholdValue << "\n";
++        }
++    }
++
++    // If made it here, parsing successful, even if zero results
++    outSuccessful = true;
++
++    return results;
++}
++
++bool RedfishChassisMatcher::fillFromJson(const nlohmann::json& json)
++{
++    clear();
++
++    redfishName = fillFromJsonString("Name", json);
++    redfishId = fillFromJsonString("Id", json);
++
++    manufacturer = fillFromJsonString("Manufacturer", json);
++    model = fillFromJsonString("Model", json);
++    partNumber = fillFromJsonString("PartNumber", json);
++    sku = fillFromJsonString("SKU", json);
++    serialNumber = fillFromJsonString("SerialNumber", json);
++    sparePartNumber = fillFromJsonString("SparePartNumber", json);
++    version = fillFromJsonString("Version", json);
++
++    // At least one characteristic must have been filled in
++    if (isEmpty())
++    {
++        std::cerr << "Trouble parsing Chassis characteristics from JSON\n";
++
++        return false;
++    }
++
++    return true;
++}
++
++bool RedfishSensorMatcher::fillFromJson(const nlohmann::json& json)
++{
++    clear();
++
++    redfishName = fillFromJsonString("Name", json);
++    redfishId = fillFromJsonString("Id", json);
++
++    // At least one characteristic must have been filled in
++    if (isEmpty())
++    {
++        std::cerr << "Trouble parsing Sensor characteristics from JSON\n";
++
++        return false;
++    }
++
++    return true;
++}
++
++bool RedfishServer::fillFromRoot(const nlohmann::json& json)
++{
++    pathChassis.clear();
++    pathTelemetry.clear();
++
++    pathChassis = fillFromJsonPath("Chassis", json);
++    pathTelemetry = fillFromJsonPath("TelemetryService", json);
++
++    // Chassis is mandatory, but Telemetry is optional
++    if (pathChassis.empty())
++    {
++        std::cerr << "Trouble parsing JSON from Redfish root\n";
++
++        return false;
++    }
++
++    return true;
++}
++
++bool RedfishServer::fillFromTelemetry(const nlohmann::json& json)
++{
++    pathMetrics.clear();
++
++    pathMetrics = fillFromJsonPath("MetricReports", json);
++
++    if (pathMetrics.empty())
++    {
++        std::cerr << "Trouble parsing JSON from Redfish Telemetry\n";
++
++        return false;
++    }
++
++    return true;
++}
++
++bool RedfishServer::fillFromMetricCollection(const nlohmann::json& json)
++{
++    reportPaths.clear();
++
++    bool successful = false;
++    reportPaths = fillFromJsonMembers(json, successful);
++
++    // It is perfectly OK to successfully read a collection of zero members
++    if (successful)
++    {
++        if constexpr (debug)
++        {
++            std::cerr << "Server " << configName << " contains "
++                      << reportPaths.size() << " reports\n";
++        }
++    }
++    else
++    {
++        std::cerr << "Trouble parsing JSON from Redfish MetricCollection\n";
++    }
++
++    haveReportPaths = successful;
++
++    return successful;
++}
++
++bool RedfishServer::fillFromMetricReport(const nlohmann::json& json)
++{
++    // Must be an object
++    if (!(json.is_object()))
++    {
++        return false;
++    }
++
++    std::string reportPath = fillFromJsonString("@odata.id", json);
++    if (reportPath.empty())
++    {
++        return false;
++    }
++
++    int sensorsMatched = 0;
++    int reportsIncluded = checkMetricReport(json, sensorsMatched);
++
++    // At this early stage, there will be no sensors matched, but we want
++    // to validate the JSON and make sure it was fetched without error.
++    if (reportsIncluded < 0)
++    {
++        return false;
++    }
++
++    auto newReport = std::make_shared<RedfishMetricReport>();
++
++    newReport->reportPath = reportPath;
++    newReport->isHelpful = false;
++    newReport->isCollected = false;
++    newReport->reportCache = json;
++
++    auto iter = pathsToMetricReports.find(reportPath);
++    if (iter != pathsToMetricReports.end())
++    {
++        iter->second.reset();
++    }
++
++    pathsToMetricReports[reportPath] = newReport;
++
++    if constexpr (debug)
++    {
++        std::cerr << "Parsed report: " << reportPath << ", " << reportsIncluded
++                  << " reports, " << sensorsMatched << " early sensors\n";
++    }
++
++    return true;
++}
++
++bool RedfishServer::fillFromChassisCollection(const nlohmann::json& json)
++{
++    chassisPaths.clear();
++
++    bool successful = false;
++    chassisPaths = fillFromJsonMembers(json, successful);
++
++    // It is perfectly OK to successfully read a collection of zero members
++    if (successful)
++    {
++        if constexpr (debug)
++        {
++            std::cerr << "Server " << configName << " contains "
++                      << chassisPaths.size() << " chassises\n";
++        }
++    }
++    else
++    {
++        std::cerr << "Trouble parsing JSON from Redfish ChassisCollection\n";
++    }
++
++    haveChassisPaths = successful;
++
++    return successful;
++}
++
++bool RedfishServer::fillFromChassisCandidate(const nlohmann::json& json,
++                                             const std::string& path)
++{
++    // Must be an object
++    if (!(json.is_object()))
++    {
++        return false;
++    }
++
++    std::string chassisPath = fillFromJsonString("@odata.id", json);
++    if (chassisPath.empty())
++    {
++        return false;
++    }
++    if (chassisPath != path)
++    {
++        std::cerr << "Redfish path mismatch: " << path << " expected, "
++                  << chassisPath << " received\n";
++
++        return false;
++    }
++
++    bool isAcceptable = true;
++
++    // It is OK for a chassis to not support sensors
++    std::string sensorsPath = fillFromJsonPath("Sensors", json);
++    if (sensorsPath.empty())
++    {
++        if constexpr (debug)
++        {
++            std::cerr << "Redfish chassis " << chassisPath
++                      << " does not support sensors\n";
++        }
++        isAcceptable = false;
++    }
++
++    RedfishChassisMatcher characteristics;
++
++    // It is OK for a chassis to not have anything we are looking for
++    if (!(characteristics.fillFromJson(json)))
++    {
++        if constexpr (debug)
++        {
++            std::cerr << "Redfish chassis " << chassisPath
++                      << " does not have characteristics\n";
++        }
++        isAcceptable = false;
++    }
++    if (characteristics.isEmpty())
++    {
++        if constexpr (debug)
++        {
++            std::cerr << "Redfish chassis " << chassisPath
++                      << " has no distinguishing characteristics\n";
++        }
++        isAcceptable = false;
++    }
++
++    auto newCandidate = std::make_shared<RedfishChassisCandidate>();
++
++    newCandidate->chassisPath = chassisPath;
++    newCandidate->sensorsPath = sensorsPath;
++    newCandidate->characteristics = characteristics;
++    newCandidate->isAcceptable = isAcceptable;
++
++    auto iter = pathsToChassisCandidates.find(chassisPath);
++    if (iter != pathsToChassisCandidates.end())
++    {
++        iter->second.reset();
++    }
++
++    pathsToChassisCandidates[chassisPath] = newCandidate;
++
++    if constexpr (debug)
++    {
++        std::cerr << "Parsed chassis candidate: " << chassisPath << ", "
++                  << (isAcceptable ? "acceptable" : "disqualified") << "\n";
++    }
++
++    return true;
++}
++
++bool RedfishServer::fillFromSensorCollection(const nlohmann::json& json,
++                                             const std::string& path)
++{
++    // Must be an object
++    if (!(json.is_object()))
++    {
++        return false;
++    }
++
++    std::string sensorCollectionPath = fillFromJsonString("@odata.id", json);
++    if (sensorCollectionPath.empty())
++    {
++        return false;
++    }
++    if (sensorCollectionPath != path)
++    {
++        std::cerr << "Redfish path mismatch: " << path << " expected, "
++                  << sensorCollectionPath << " received\n";
++
++        return false;
++    }
++
++    bool successful = false;
++    std::vector<std::string> sensorPaths =
++        fillFromJsonMembers(json, successful);
++
++    // It is perfectly OK to successfully read a collection of zero members
++    if (!successful)
++    {
++        std::cerr << "Trouble parsing JSON from Redfish SensorCollection\n";
++
++        return false;
++    }
++
++    size_t numPathMatches = 0;
++    std::shared_ptr<RedfishChassisCandidate> candidatePtr;
++
++    // This sensor list must appear within exactly one chassis candidate
++    for (const auto& chassisPair : pathsToChassisCandidates)
++    {
++        if (!(chassisPair.second->isAcceptable))
++        {
++            continue;
++        }
++
++        if (chassisPair.second->sensorsPath == sensorCollectionPath)
++        {
++            ++numPathMatches;
++            candidatePtr = chassisPair.second;
++
++            if constexpr (debug)
++            {
++                std::cerr << "Chassis " << candidatePtr->chassisPath
++                          << " contains sensors: " << sensorCollectionPath
++                          << "\n";
++            }
++        }
++    }
++
++    if (numPathMatches != 1)
++    {
++        std::cerr << "Trouble matching up SensorCollection with Chassis: "
++                  << sensorCollectionPath << "\n";
++
++        return false;
++    }
++
++    // Now that we know which candidate it was, can finally store results
++    candidatePtr->sensorPaths = sensorPaths;
++    candidatePtr->haveSensorPaths = successful;
++
++    if constexpr (debug)
++    {
++        std::cerr << "Chassis " << candidatePtr->chassisPath << " has "
++                  << sensorPaths.size() << " sensors\n";
++    }
++
++    // A chassis with zero sensors is valid Redfish but uninteresting to us
++    if (sensorPaths.empty())
++    {
++        candidatePtr->isAcceptable = false;
++
++        if constexpr (debug)
++        {
++            std::cerr << "Redfish chassis " << candidatePtr->chassisPath
++                      << " has no sensors\n";
++        }
++    }
++
++    return successful;
++}
++
++bool RedfishServer::fillFromSensorCandidate(const nlohmann::json& json,
++                                            const std::string& path)
++{
++    // Must be an object
++    if (!(json.is_object()))
++    {
++        return false;
++    }
++
++    std::string sensorPath = fillFromJsonString("@odata.id", json);
++    if (sensorPath.empty())
++    {
++        return false;
++    }
++    if (sensorPath != path)
++    {
++        std::cerr << "Redfish path mismatch: " << path << " expected, "
++                  << sensorPath << " received\n";
++
++        return false;
++    }
++
++    bool isAcceptable = true;
++
++    RedfishSensorMatcher characteristics;
++
++    // It is OK for a sensor to not have anything we are looking for
++    if (!(characteristics.fillFromJson(json)))
++    {
++        std::cerr << "Redfish sensor " << sensorPath
++                  << " does not have characteristics\n";
++        isAcceptable = false;
++    }
++    if (characteristics.isEmpty())
++    {
++        std::cerr << "Redfish sensor " << sensorPath
++                  << " has no distinguishing characteristics\n";
++        isAcceptable = false;
++    }
++
++    auto newCandidate = std::make_shared<RedfishSensorCandidate>();
++
++    newCandidate->sensorPath = sensorPath;
++    newCandidate->characteristics = characteristics;
++    newCandidate->isAcceptable = isAcceptable;
++    newCandidate->readingCache = json;
++
++    size_t numPathMatches = 0;
++    std::shared_ptr<RedfishChassisCandidate> candidatePtr;
++
++    // This sensor path must appear within exactly one chassis candidate
++    for (const auto& chassisPair : pathsToChassisCandidates)
++    {
++        if (!(chassisPair.second->isAcceptable))
++        {
++            continue;
++        }
++
++        for (const std::string& sensorCandidatePath :
++             chassisPair.second->sensorPaths)
++        {
++            if (sensorPath == sensorCandidatePath)
++            {
++                ++numPathMatches;
++                candidatePtr = chassisPair.second;
++
++                if constexpr (debug)
++                {
++                    std::cerr << "Chassis " << candidatePtr->chassisPath
++                              << " has sensor " << sensorPath << "\n";
++                }
++            }
++        }
++    }
++
++    if (numPathMatches != 1)
++    {
++        std::cerr << "Trouble matching up Sensor with Chassis: " << sensorPath
++                  << "\n";
++
++        return false;
++    }
++
++    auto iter = candidatePtr->pathsToSensorCandidates.find(sensorPath);
++    if (iter != candidatePtr->pathsToSensorCandidates.end())
++    {
++        iter->second.reset();
++    }
++
++    candidatePtr->pathsToSensorCandidates[sensorPath] = newCandidate;
++
++    if constexpr (debug)
++    {
++        std::cerr << "Parsed sensor candidate: " << sensorPath << ", chassis "
++                  << candidatePtr->chassisPath << ", "
++                  << (isAcceptable ? "acceptable" : "disqualified") << "\n";
++    }
++
++    return true;
++}
++
++bool RedfishServer::fillFromSensorAccepted(const nlohmann::json& json)
++{
++    // Must be an object
++    if (!(json.is_object()))
++    {
++        return false;
++    }
++
++    // The object must contain this hardcoded field name
++    std::string sensorPath = fillFromJsonString("@odata.id", json);
++    if (sensorPath.empty())
++    {
++        return false;
++    }
++
++    // Discovery must already have ran, and filled this in by now
++    auto iterPair = pathsToSensors.find(sensorPath);
++    if (iterPair == pathsToSensors.end())
++    {
++        return false;
++    }
++
++    RedfishSensor& sensor = *(iterPair->second);
++
++    // The object must contain at least one of these two ways to define units
++    std::string readingUnits = fillFromJsonString("ReadingUnits", json);
++    std::string readingType = fillFromJsonString("ReadingType", json);
++
++    std::string goodLookupSource;
++    std::string dbusByUnits;
++    std::string dbusByType;
++
++    if (!(readingUnits.empty()))
++    {
++        dbusByUnits = unitLookup.lookup(readingUnits).dbusUnits;
++        if (!(dbusByUnits.empty()))
++        {
++            goodLookupSource = dbusByUnits;
++        }
++    }
++    if (!(readingType.empty()))
++    {
++        dbusByType = unitLookup.lookup(readingType).dbusUnits;
++        if (!(dbusByType.empty()))
++        {
++            goodLookupSource = dbusByType;
++        }
++    }
++
++    // At least one must have been resolved
++    if (goodLookupSource.empty())
++    {
++        std::cerr << "Sensor " << sensor.configName
++                  << " Redfish units missing\n";
++        return false;
++    }
++
++    // If only one was resolved, we take whatever we could get
++    if (dbusByUnits.empty() || dbusByType.empty())
++    {
++        if constexpr (debug)
++        {
++            std::cerr << "Sensor " << sensor.configName
++                      << " Redfish units partially specified but still OK: "
++                      << goodLookupSource << "\n";
++        }
++    }
++    else
++    {
++        // If both were resolved, both must agree
++        if (dbusByUnits != dbusByType)
++        {
++            std::cerr << "Sensor " << sensor.configName
++                      << " Redfish units discrepancy: " << dbusByUnits << " vs "
++                      << dbusByType << "\n";
++            return false;
++        }
++    }
++
++    // Confirmed good lookup source and preflight, now do the real lookups
++    std::string dbusUnit = unitLookup.lookup(goodLookupSource).dbusUnits;
++    double defaultMin = unitLookup.lookup(goodLookupSource).rangeMin;
++    double defaultMax = unitLookup.lookup(goodLookupSource).rangeMax;
++
++    // These are optional, but if specified, both must be specified
++    bool successfulMin = false;
++    bool successfulMax = false;
++    double rangeMin =
++        fillFromJsonNumber("ReadingRangeMin", json, successfulMin);
++    double rangeMax =
++        fillFromJsonNumber("ReadingRangeMax", json, successfulMax);
++
++    if (successfulMin && successfulMax && std::isfinite(rangeMin) &&
++        std::isfinite(rangeMax))
++    {
++        if constexpr (debug)
++        {
++            std::cerr << "Sensor " << sensor.configName
++                      << " Redfish range: min " << rangeMin << ", max "
++                      << rangeMax << "\n";
++        }
++    }
++    else
++    {
++        rangeMin = defaultMin;
++        rangeMax = defaultMax;
++    }
++
++    if (!(rangeMin < rangeMax))
++    {
++        std::cerr << "Sensor " << sensor.configName
++                  << " Redfish range unusable: min " << rangeMin << ", max "
++                  << rangeMax << "\n";
++        return false;
++    }
++
++    std::vector<thresholds::Threshold> thresholds;
++
++    auto findThresholds = json.find("Thresholds");
++    if (findThresholds != json.end())
++    {
++        bool successful = false;
++        thresholds = parseThresholds(*findThresholds, successful);
++
++        if (successful)
++        {
++            if constexpr (debug)
++            {
++                std::cerr << "Sensor " << sensor.configName << " has "
++                          << thresholds.size() << " thresholds\n";
++            }
++        }
++        else
++        {
++            std::cerr << "Trouble parsing Redfish thresholds: "
++                      << sensor.configName << "\n";
++            return false;
++        }
++    }
++
++    if constexpr (debug)
++    {
++        std::cerr << "Sensor " << sensor.configName
++                  << " successfully parsed from " << sensorPath << "\n";
++    }
++
++    // Stuff in what we have learned from the Redfish sensor object
++    // The matcher must have already filled in redfishPath and chassisPath
++    sensor.units = dbusUnit;
++    sensor.minValue = rangeMin;
++    sensor.maxValue = rangeMax;
++    sensor.thresholds = thresholds;
++
++    return true;
++}
++
++bool RedfishServer::acceptSensorReading(
++    const nlohmann::json& json,
++    const std::chrono::steady_clock::time_point& now)
++{
++    // Must be an object
++    if (!(json.is_object()))
++    {
++        return false;
++    }
++
++    // The object must contain this hardcoded field name
++    std::string sensorPath = fillFromJsonString("@odata.id", json);
++    if (sensorPath.empty())
++    {
++        return false;
++    }
++
++    // The value must be present, even if it is NaN
++    bool successful = false;
++    double sensorValue = fillFromJsonNumber("Reading", json, successful);
++    if (!successful)
++    {
++        return false;
++    }
++
++    // The sensor must have made it through discovery
++    auto iterPair = pathsToSensors.find(sensorPath);
++    if (iterPair == pathsToSensors.end())
++    {
++        return false;
++    }
++
++    RedfishSensor& sensor = *(iterPair->second);
++
++    // The sensor must have successfully been instantiated
++    if (!(sensor.impl))
++    {
++        return false;
++    }
++
++    // Sensor found, add good reading from this report
++    // OK if NaN, intentionally not testing isfinite(sensorValue) here
++    sensor.readingValue = sensorValue;
++    sensor.readingWhen = now;
++    sensor.isCollected = true;
++    sensor.impl->updateValue(sensor.readingValue);
++    ++readingsGoodIndividual;
++
++    if constexpr (debug)
++    {
++        std::cerr << "Accepted individual reading: sensor " << sensor.configName
++                  << " = " << sensorValue << "\n";
++    }
++
++    return true;
++}
++
++// This is fast path, code should be optimized here, run as fast as possible
++bool RedfishServer::acceptMetricReport(
++    const nlohmann::json& json,
++    const std::chrono::steady_clock::time_point& now)
++{
++    // Must be an object
++    if (!(json.is_object()))
++    {
++        return false;
++    }
++
++    // The object must contain this hardcoded field name
++    std::string reportPath = fillFromJsonString("@odata.id", json);
++    if (reportPath.empty())
++    {
++        return false;
++    }
++
++    // The object must contain this hardcoded field name
++    auto iterValues = json.find("MetricValues");
++    if (iterValues == json.end())
++    {
++        return false;
++    }
++
++    // The field value must be an array
++    if (!(iterValues->is_array()))
++    {
++        return false;
++    }
++
++    // This should already have been filled in during discovery
++    auto iterReport = pathsToMetricReports.find(reportPath);
++    if (iterReport == pathsToMetricReports.end())
++    {
++        return false;
++    }
++
++    // Unlike Members, this JSON contains no corresponding "@odata.count"
++    size_t count = iterValues->size();
++
++    // FUTURE: Consider keeping track of badReadings also
++    size_t goodReadings = 0;
++
++    for (size_t i = 0; i < count; ++i)
++    {
++        const auto& element = (*iterValues)[i];
++
++        // The element must be an object
++        if (!(element.is_object()))
++        {
++            continue;
++        }
++
++        // This indicates the sensor whose reading is being supplied here
++        std::string sensorPath = fillFromJsonString("MetricProperty", element);
++        if (sensorPath.empty())
++        {
++            continue;
++        }
++
++        // The value must be present, even if it is NaN
++        bool successful = false;
++        double sensorValue =
++            fillFromJsonNumber("MetricValue", element, successful);
++        if (!successful)
++        {
++            continue;
++        }
++
++        // This should already have been filled in during discovery
++        auto iterPair = pathsToSensors.find(sensorPath);
++        if (iterPair == pathsToSensors.end())
++        {
++            continue;
++        }
++
++        RedfishSensor& sensor = *(iterPair->second);
++
++        // The sensor must have successfully been instantiated
++        if (!(sensor.impl))
++        {
++            continue;
++        }
++
++        // Sensor found, add good reading from this report
++        // OK if NaN, intentionally not testing isfinite(sensorValue) here
++        sensor.readingValue = sensorValue;
++        sensor.readingWhen = now;
++        sensor.isCollected = true;
++        sensor.impl->updateValue(sensor.readingValue);
++        ++readingsGoodReported;
++
++        if constexpr (debug)
++        {
++            std::cerr << "Accepted report[" << i << "]: sensor "
++                      << sensor.configName << " = " << sensorValue << "\n";
++        }
++
++        ++goodReadings;
++    }
++
++    // Mark this report as having been successfully collected
++    iterReport->second->isCollected = true;
++
++    ++readingsReports;
++
++    if constexpr (debug)
++    {
++        std::cerr << "Accepted report: " << goodReadings << " good readings\n";
++    }
++
++    // It is good if we got this far, even if zero readings
++    return true;
++}
++
++// Used twice during Discovery, to validate existence, then to see if relevant
++int RedfishServer::checkMetricReport(const nlohmann::json& json,
++                                     int& outMatched)
++{
++    outMatched = 0;
++
++    // Must be an object
++    if (!(json.is_object()))
++    {
++        return -1;
++    }
++
++    // OK during preflight for report to not be in pathsToMetricReports
++    // The object must contain this hardcoded field name
++    auto iterValues = json.find("MetricValues");
++    if (iterValues == json.end())
++    {
++        return -1;
++    }
++
++    // The field value must be an array
++    if (!(iterValues->is_array()))
++    {
++        return -1;
++    }
++
++    // Unlike Members, this JSON contains no corresponding "@odata.count"
++    size_t count = iterValues->size();
++    int goodChecks = 0;
++
++    for (size_t i = 0; i < count; ++i)
++    {
++        const auto& element = (*iterValues)[i];
++
++        // The element must be an object
++        if (!(element.is_object()))
++        {
++            continue;
++        }
++
++        // This indicates the sensor whose reading is being supplied here
++        std::string sensorPath = fillFromJsonString("MetricProperty", element);
++        if (sensorPath.empty())
++        {
++            continue;
++        }
++
++        // The value must be present in the JSON, even if it is NaN
++        // This is only a preflight, do not store the returned value
++        bool successful = false;
++        fillFromJsonNumber("MetricValue", element, successful);
++        if (!successful)
++        {
++            continue;
++        }
++
++        // This report looks like it would be useful for a sensor
++        ++goodChecks;
++
++        // OK to not find any during preflight, as pathsToSensors not yet set
++        auto iterPair = pathsToSensors.find(sensorPath);
++        if (iterPair == pathsToSensors.end())
++        {
++            continue;
++        }
++
++        // This report will be helpful to at least one of our sensors
++        // Do nothing more with it, as this is just a preflight
++        if constexpr (debug)
++        {
++            // Redo the fill, because returned value now useful for debugging
++            double sensorValue =
++                fillFromJsonNumber("MetricValue", element, successful);
++
++            std::cerr << "Checked report[" << i << "]: sensor "
++                      << iterPair->second->configName << " = " << sensorValue
++                      << "\n";
++        }
++
++        ++outMatched;
++    }
++
++    if constexpr (debug)
++    {
++        std::cerr << "Checked report: " << goodChecks << " readings, "
++                  << outMatched << " matched sensors\n";
++    }
++
++    return goodChecks;
++}
+diff --git a/src/meson.build b/src/meson.build
+index a19030b..f2a343a 100644
+--- a/src/meson.build
++++ b/src/meson.build
+@@ -213,3 +213,22 @@ if get_option('external').enabled()
+         install: true,
+     )
+ endif
++
++if get_option('redfish').enabled()
++    executable(
++        'redfishsensor',
++        'RedfishSensor.cpp',
++        'RedfishSensorMain.cpp',
++        'RedfishSensorQuery.cpp',
++        'RedfishSensorResponse.cpp',
++        dependencies: [
++            default_deps,
++            thresholds_dep,
++            utils_dep,
++        ],
++        cpp_args: uring_args,
++        implicit_include_directories: false,
++        include_directories: '../include',
++        install: true,
++    )
++endif
+-- 
+2.39.1.581.gbfd45094c4-goog
+
diff --git a/recipes-phosphor/sensors/dbus-sensors_%.bbappend b/recipes-phosphor/sensors/dbus-sensors_%.bbappend
index 93b922b..e6262ae 100644
--- a/recipes-phosphor/sensors/dbus-sensors_%.bbappend
+++ b/recipes-phosphor/sensors/dbus-sensors_%.bbappend
@@ -1,12 +1,13 @@
 FILESEXTRAPATHS:prepend:gbmc := "${THISDIR}/${PN}:"
 
 SRC_URI:append:gbmc = " \
-    file://0002-psusensor-attempt-to-re-open-hwmon-files-if-reading-.patch \
-    file://0003-gpiosensor-a-dedicated-daemon-to-report-gpio-status-.patch \
-    file://0004-Feature-Add-association-interfaces-to-a-target-EM.patch \
-    file://0001-dbus-sensors-Associate-PSU-sensors-correctly.patch \
-    file://0005-dbus-sensors-Associate-CPU-inventory-with-CPU-sensor.patch \
-    file://0006-cpusensor-Associate-DIMM-inventory-with-sensors.patch \
+  file://0002-psusensor-attempt-to-re-open-hwmon-files-if-reading-.patch \
+  file://0003-gpiosensor-a-dedicated-daemon-to-report-gpio-status-.patch \
+  file://0004-Feature-Add-association-interfaces-to-a-target-EM.patch \
+  file://0005-dbus-sensors-Associate-CPU-inventory-with-CPU-sensor.patch \
+  file://0006-cpusensor-Associate-DIMM-inventory-with-sensors.patch \
+  file://0001-dbus-sensors-Associate-PSU-sensors-correctly.patch \
+  file://0101-RedfishSensor-It-reads-Redfish-sensors-to-D-Bus.patch \
 "
 
 # TODO(linchuyuan@google.com): remove the following section once the upstream has the change
@@ -17,3 +18,9 @@
                                                'xyz.openbmc_project.gpiosensor.service', \
                                                '', d)}"
 
+# RedfishSensor is enabled similarly to GPIOSensor
+# TODO(krellan@google.com): As above, remove once b/266550783 upstreamed
+PACKAGECONFIG:append:gbmc = " redfishsensor"
+PACKAGECONFIG[redfishsensor] = "-Dredfish=enabled, -Dredfish=disabled"
+SYSTEMD_SERVICE:${PN} += "${@bb.utils.contains('PACKAGECONFIG', \
+  'redfishsensor', 'xyz.openbmc_project.redfishsensor.service', '', d)}"