meta-hgxr: nsmd/libnsm TAV Mode Index 20 binding

Add libnsm NSM Type 5 Device Mode Index 20 (TAV Mode) codec
support and the nsmd QTM4 handler/D-Bus binding, structurally
mirroring the merged Power Capping Index 27 implementation.

Byte layout (enum8: Default=0/Enabled=1/Disabled=2, 1-byte
payload) mirrors Index 27 structurally and by value assignment.
No written NSM Index 20 contract exists yet -- this is a
best-effort mirror per engineer instruction and must be
verified against a written NSM API contract before this ships.

Changes:
- libnsm/device-configuration.h: DEVICE_MODE_TAV = 20,
  nsm_tav_mode enum, TAV_MODE_DATA_SIZE, with caveat comments.
- libnsm/test/libnsm_device_configuration_test.cpp: tavModeV2
  suite mirroring powerCappingModeV2.
- nsmd/nsmDeviceInventory/nsmSwitch.hpp/.cpp: NsmSwitchTAVMode
  sensor class, toTAVModeFromGet, genRequestMsg,
  handleResponseMsg, setTAVMode, createNsmSwitchTAVMode, gated
  on entity-manager SupportTAVMode in createNsmSwitchDI.
- nsmd/nsmDeviceInventory/test/nsmSwitchFactoryBranch_test.cpp:
  TAVMode test block mirroring PowerCappingMode coverage.
- nsmtool/nsm_config_cmd.cpp: GetTAVMode/SetTAVMode subcommands.

Depends on PDI DGXOPENBMC-28636 (com.nvidia.DeviceMode.TAVMode)
for the generated server.hpp header used by nsmSwitch.hpp.

Review fixups applied:
- Removed internal ticket-ID references and "mirrors power
  capping" style comments from public source/header/test
  comments (kept the technical rationale).
- SupportTAVMode gate now reads via the existing
  dbusPropertyMapAsBool helper instead of a raw
  std::get_if<bool>, so an integer-encoded EM value is
  normalized instead of silently resolving to false.
- createNsmSwitchTAVMode kept inline (not static), matching
  the established file-local factory-helper convention used by
  createNsmSwitchPowerCappingMode and
  createNsmSwitchL1PredictionMode in this file.
- GetTAVMode::parseResponseMsg rejects a missing/short
  CurrentMode instead of silently omitting it, matching the
  GetLTXMode/GetUPhyMode mandatory-CurrentMode convention.
- Do not resolve Default on D-Bus publish: nsmd does not
  resolve a wire Default (0) CurrentMode/PendingMode to a
  guessed Enabled before publishing to D-Bus, on either the
  GET path or after a successful reset-to-default Set. No
  written contract or arch sign-off confirms what TAV's
  default actually is; the design doc only says it comes from
  QM4's own INI configuration, and the identical guess-on-
  mirror pattern was already wrong once for UPhy mode (commit
  c7c3b2b4). nsmd now publishes the wire value as-is; bmcweb's
  TAVMode handlers already treat a still-Default
  CurrentMode/PendingMode as not-yet-resolved by firmware,
  matching the existing LTXMode/UPhyRecoveryMode contract.

Note: the nsmtool GetTAVMode CLI display intentionally still
prints raw "Default" for wire value 0 (not "Enabled"),
matching the established GetPowerCappingMode/GetLTXMode/
GetUPhyMode CLI precedent in this file. A prior review
comment suggested mapping Default to Enabled in the CLI to
match D-Bus read semantics, but that no longer applies now
that nsmd does not resolve Default to Enabled on the D-Bus
side either -- see the note above. Flagging for tech-lead
call rather than reintroducing the guess.

Test status: local build blocked in this environment by a
missing dbus-1 pkg-config dependency (nsmtool/meson.build);
clang-format-20 clean on all changed files. No lab/unit run
performed -- pending CI and PDI MR merge.

Fixes jira https://jirasw.nvidia.com/browse/DGXOPENBMC-28638

Signed-off-by: Surya Garimella <sgarimella@nvidia.com>
diff --git a/libnsm/device-configuration.h b/libnsm/device-configuration.h
index 8d23042..d7956fd 100644
--- a/libnsm/device-configuration.h
+++ b/libnsm/device-configuration.h
@@ -605,6 +605,7 @@
 	DEVICE_MODE_PERSISTENT_CPU_POWER_LIMIT_GPU_COPY = 14,
 	DEVICE_MODE_ONE_SHOT_GPU_COPY_SWITCH_POWER_LIMIT = 15,
 	DEVICE_MODE_PERSISTENT_GPU_COPY_SWITCH_POWER_LIMIT = 16,
+	DEVICE_MODE_TAV = 20,
 	DEVICE_MODE_ADAPTIVE_TGPMODE = 21,
 	DEVICE_MODE_LLDP = 24,
 	DEVICE_MODE_PROTECTION_OPTIONS_MODE = 26,
@@ -640,6 +641,18 @@
 /** @brief Data size (in bytes) of the Power Capping Mode (index 27) payload. */
 #define POWER_CAPPING_MODE_DATA_SIZE 1
 
+/** @brief TAV (Temperature Aware Voltage) Mode values per NSM Type 5 Device
+ *         Mode Index 20 (DEVICE_MODE_TAV). Single enum8 data byte.
+ */
+enum nsm_tav_mode {
+	NSM_TAV_MODE_DEFAULT = 0,
+	NSM_TAV_MODE_ENABLED = 1,
+	NSM_TAV_MODE_DISABLED = 2,
+};
+
+/** @brief Data size (in bytes) of the TAV Mode (index 20) payload. */
+#define TAV_MODE_DATA_SIZE 1
+
 /** @brief LTX (Link Training Extended) Mode values per NSM Type 5 Device Mode
  *         Index 29 (DEVICE_MODE_LTX). Single enum8 data byte. Non-volatile;
  *         requires a link toggle to take effect.
diff --git a/libnsm/test/libnsm_device_configuration_test.cpp b/libnsm/test/libnsm_device_configuration_test.cpp
index 781fa89..394595b 100644
--- a/libnsm/test/libnsm_device_configuration_test.cpp
+++ b/libnsm/test/libnsm_device_configuration_test.cpp
@@ -4092,3 +4092,161 @@
 	    &current_len, &pending_mode, &pending_len);
 	EXPECT_EQ(rc, NSM_SW_ERROR_LENGTH);
 }
+
+// ---------------------------------------------------------------------------
+// TAV (Temperature Aware Voltage) Mode (NSM Type 5 Device Mode Index 20,
+// enum8). enum8 {0 Default, 1 Enabled, 2 Disabled}; 1-byte data via the
+// generic v2 codec.
+// ---------------------------------------------------------------------------
+
+TEST(tavModeV2, enumValues)
+{
+	EXPECT_EQ(DEVICE_MODE_TAV, 20);
+	EXPECT_EQ(NSM_TAV_MODE_DEFAULT, 0);
+	EXPECT_EQ(NSM_TAV_MODE_ENABLED, 1);
+	EXPECT_EQ(NSM_TAV_MODE_DISABLED, 2);
+	EXPECT_EQ(TAV_MODE_DATA_SIZE, 1);
+}
+
+TEST(tavModeV2, setEncodeRequestEnabled)
+{
+	uint32_t device_mode_index = DEVICE_MODE_TAV;
+	uint8_t mode = NSM_TAV_MODE_ENABLED;
+
+	std::vector<uint8_t> requestMsg(
+	    sizeof(nsm_msg_hdr) + sizeof(nsm_set_device_mode_settings_v2_req) +
+		TAV_MODE_DATA_SIZE - 1,
+	    0);
+	auto request = reinterpret_cast<nsm_msg *>(requestMsg.data());
+
+	auto rc = encode_set_device_mode_settings_v2_req(
+	    0, device_mode_index, &mode, TAV_MODE_DATA_SIZE, request);
+
+	struct nsm_set_device_mode_settings_v2_req *req =
+	    reinterpret_cast<struct nsm_set_device_mode_settings_v2_req *>(
+		request->payload);
+
+	EXPECT_EQ(rc, NSM_SW_SUCCESS);
+	EXPECT_EQ(NSM_SET_DEVICE_MODE_SETTINGS_V2, req->hdr.command);
+	EXPECT_EQ(sizeof(req->device_mode_index) + TAV_MODE_DATA_SIZE,
+		  req->hdr.data_size);
+	EXPECT_EQ(device_mode_index, le32toh(req->device_mode_index));
+}
+
+TEST(tavModeV2, setDecodeRequestDisabled)
+{
+	uint32_t device_mode_index = DEVICE_MODE_TAV;
+	uint8_t mode = NSM_TAV_MODE_DISABLED;
+
+	std::vector<uint8_t> requestMsg(
+	    sizeof(nsm_msg_hdr) + sizeof(nsm_set_device_mode_settings_v2_req) +
+		TAV_MODE_DATA_SIZE - 1,
+	    0);
+	auto request = reinterpret_cast<nsm_msg *>(requestMsg.data());
+
+	encode_set_device_mode_settings_v2_req(0, device_mode_index, &mode,
+					       TAV_MODE_DATA_SIZE, request);
+
+	uint32_t decoded_index = 0;
+	uint8_t decoded_data[TAV_MODE_DATA_SIZE] = {0};
+	uint16_t decoded_data_length = 0;
+
+	auto rc = decode_set_device_mode_settings_v2_req(
+	    request, requestMsg.size(), &decoded_index, decoded_data,
+	    &decoded_data_length);
+
+	EXPECT_EQ(rc, NSM_SW_SUCCESS);
+	EXPECT_EQ(device_mode_index, decoded_index);
+	EXPECT_EQ(TAV_MODE_DATA_SIZE, decoded_data_length);
+	EXPECT_EQ(NSM_TAV_MODE_DISABLED, decoded_data[0]);
+}
+
+TEST(tavModeV2, getEncodeRequest)
+{
+	std::vector<uint8_t> requestMsg(
+	    sizeof(nsm_msg_hdr) + sizeof(nsm_get_device_mode_settings_v2_req),
+	    0);
+	auto request = reinterpret_cast<nsm_msg *>(requestMsg.data());
+
+	auto rc =
+	    encode_get_device_mode_settings_v2_req(0, DEVICE_MODE_TAV, request);
+
+	uint32_t decoded_index = 0;
+	EXPECT_EQ(rc, NSM_SW_SUCCESS);
+	EXPECT_EQ(decode_get_device_mode_settings_v2_req(
+		      request, requestMsg.size(), &decoded_index),
+		  NSM_SW_SUCCESS);
+	EXPECT_EQ(DEVICE_MODE_TAV, decoded_index);
+}
+
+TEST(tavModeV2, supportedListIncludesIndex20)
+{
+	const uint16_t handle = 0x0000;
+	const uint16_t mode_count = 2;
+	const uint32_t mode_list[] = {DEVICE_MODE_LLDP, DEVICE_MODE_TAV};
+
+	std::vector<uint8_t> responseMsg(
+	    sizeof(nsm_msg_hdr) + sizeof(nsm_get_supported_device_modes_resp) +
+		(mode_count - 1) * sizeof(uint32_t),
+	    0);
+	auto response = reinterpret_cast<nsm_msg *>(responseMsg.data());
+
+	ASSERT_EQ(encode_get_supported_device_modes_resp(
+		      0, NSM_SUCCESS, ERR_NULL, handle, mode_count, mode_list,
+		      response),
+		  NSM_SW_SUCCESS);
+
+	uint8_t cc = 0;
+	uint16_t reason_code = 0;
+	uint16_t decoded_handle = 0;
+	uint16_t decoded_mode_count = 0;
+	uint32_t decoded_mode_list[2] = {0};
+
+	ASSERT_EQ(decode_get_supported_device_modes_resp(
+		      response, responseMsg.size(), &cc, &reason_code,
+		      &decoded_handle, &decoded_mode_count, decoded_mode_list),
+		  NSM_SW_SUCCESS);
+	EXPECT_EQ(NSM_SUCCESS, cc);
+	EXPECT_EQ(mode_count, decoded_mode_count);
+
+	bool found = false;
+	for (uint16_t i = 0; i < decoded_mode_count; i++) {
+		if (decoded_mode_list[i] == DEVICE_MODE_TAV) {
+			found = true;
+		}
+	}
+	EXPECT_TRUE(found);
+}
+
+TEST(tavModeV2, setEncodeRejectsNullDataWithLength)
+{
+	/* Bounds guard: encode must fail when length > 0 but data pointer is
+	 * null. */
+	std::vector<uint8_t> requestMsg(
+	    sizeof(nsm_msg_hdr) + sizeof(nsm_set_device_mode_settings_v2_req) +
+		TAV_MODE_DATA_SIZE - 1,
+	    0);
+	auto request = reinterpret_cast<nsm_msg *>(requestMsg.data());
+	auto rc = encode_set_device_mode_settings_v2_req(
+	    0, DEVICE_MODE_TAV, nullptr, TAV_MODE_DATA_SIZE, request);
+	EXPECT_EQ(rc, NSM_SW_ERROR_NULL);
+}
+
+TEST(tavModeV2, getDecodeRejectsTruncatedResponse)
+{
+	/* Oversized-buffer / truncated-response regression for the shared v2
+	 * codec used by index 20: msg_len shorter than the fixed header must
+	 * fail. */
+	std::vector<uint8_t> responseMsg(sizeof(nsm_msg_hdr) + 4, 0);
+	auto response = reinterpret_cast<nsm_msg *>(responseMsg.data());
+	uint8_t cc = 0;
+	uint16_t reason_code = 0;
+	uint8_t current_mode = 0;
+	uint8_t pending_mode = 0;
+	uint16_t current_len = 0;
+	uint16_t pending_len = 0;
+	auto rc = decode_get_device_mode_settings_v2_resp(
+	    response, responseMsg.size(), &cc, &reason_code, &current_mode,
+	    &current_len, &pending_mode, &pending_len);
+	EXPECT_EQ(rc, NSM_SW_ERROR_LENGTH);
+}
diff --git a/nsmd/nsmDeviceInventory/nsmSwitch.cpp b/nsmd/nsmDeviceInventory/nsmSwitch.cpp
index 48dc929..d8a31c2 100644
--- a/nsmd/nsmDeviceInventory/nsmSwitch.cpp
+++ b/nsmd/nsmDeviceInventory/nsmSwitch.cpp
@@ -963,6 +963,251 @@
                                   nvSwitchPowerCappingMode, device});
 }
 
+// Map NSM wire enum8 to TAVMode. Wire Default (0) is published to D-Bus
+// as-is; nsmd does not resolve it to a concrete Enabled/Disabled value.
+// Only the device knows what its configured default is -- bmcweb already
+// treats a still-Default CurrentMode/PendingMode as "not yet resolved by
+// firmware" (skips the property on Settings, errors on the active
+// resource) rather than guessing, matching LTXMode/UPhyRecoveryMode.
+static std::optional<TAVMode> toTAVModeFromGet(uint8_t nsmMode)
+{
+    switch (nsmMode)
+    {
+        case NSM_TAV_MODE_ENABLED:
+            return TAVMode::Enabled;
+        case NSM_TAV_MODE_DISABLED:
+            return TAVMode::Disabled;
+        case NSM_TAV_MODE_DEFAULT:
+            return TAVMode::Default;
+        default:
+            return std::nullopt;
+    }
+}
+
+std::optional<std::vector<uint8_t>>
+    NsmSwitchTAVMode::genRequestMsg(eid_t eid, uint8_t instanceId)
+{
+    std::vector<uint8_t> request(sizeof(nsm_msg_hdr) +
+                                 sizeof(nsm_get_device_mode_settings_v2_req));
+    auto requestPtr = reinterpret_cast<struct nsm_msg*>(request.data());
+    auto rc = encode_get_device_mode_settings_v2_req(
+        instanceId, DEVICE_MODE_TAV, requestPtr);
+    if (rc != NSM_SW_SUCCESS)
+    {
+        lg2::debug("encode_get_device_mode_settings_v2_req failed. "
+                   "eid={EID} rc={RC}",
+                   "EID", eid, "RC", rc);
+        return std::nullopt;
+    }
+    return request;
+}
+
+uint8_t NsmSwitchTAVMode::handleResponseMsg(const struct nsm_msg* responseMsg,
+                                            size_t responseLen)
+{
+    uint8_t cc = NSM_ERROR;
+    uint16_t reason_code = ERR_NULL;
+    uint8_t currentMode = 0;
+    uint8_t pendingMode = 0;
+    uint16_t currentModeLength = 0;
+    uint16_t pendingModeLength = 0;
+
+    /* Probe lengths with null data pointers so decode cannot overflow the
+     * 1-byte mode buffers below on a malformed oversized payload. */
+    auto rc = decode_get_device_mode_settings_v2_resp(
+        responseMsg, responseLen, &cc, &reason_code, nullptr,
+        &currentModeLength, nullptr, &pendingModeLength);
+    if (rc != NSM_SW_SUCCESS || cc != NSM_SUCCESS)
+    {
+        return cc ? cc : rc;
+    }
+    if (currentModeLength != TAV_MODE_DATA_SIZE ||
+        (pendingModeLength != 0 && pendingModeLength != TAV_MODE_DATA_SIZE))
+    {
+        return NSM_SW_ERROR_LENGTH;
+    }
+
+    rc = decode_get_device_mode_settings_v2_resp(
+        responseMsg, responseLen, &cc, &reason_code, &currentMode,
+        &currentModeLength, &pendingMode, &pendingModeLength);
+
+    if (rc == NSM_SW_SUCCESS && cc == NSM_SUCCESS)
+    {
+        if (currentModeLength != TAV_MODE_DATA_SIZE)
+        {
+            return NSM_SW_ERROR_LENGTH;
+        }
+        auto current = toTAVModeFromGet(currentMode);
+        if (!current)
+        {
+            return NSM_SW_ERROR_DATA;
+        }
+        // Publish the wire value as-is -- nsmd does not decide what
+        // Default resolves to. bmcweb's active-resource handler already
+        // treats a still-Default CurrentMode as not-yet-resolved and
+        // surfaces it as an error rather than accepting a guess.
+        tavModeIntf->currentMode(*current);
+
+        if (pendingModeLength != 0 && pendingModeLength != TAV_MODE_DATA_SIZE)
+        {
+            return NSM_SW_ERROR_LENGTH;
+        }
+        if (pendingModeLength == TAV_MODE_DATA_SIZE)
+        {
+            auto pending = toTAVModeFromGet(pendingMode);
+            if (!pending)
+            {
+                return NSM_SW_ERROR_DATA;
+            }
+            // Same as above: publish as-is. bmcweb's Settings handler
+            // already skips a still-Default PendingMode until a later
+            // poll reports the device's resolved value.
+            tavModeIntf->pendingMode(*pending);
+        }
+        else
+        {
+            // No pending override from device; keep Settings in sync.
+            tavModeIntf->pendingMode(*current);
+        }
+    }
+    return cc ? cc : rc;
+}
+
+requester::Coroutine NsmSwitchTAVMode::setTAVMode(
+    const AsyncSetOperationValueType& value,
+    [[maybe_unused]] AsyncOperationStatusType* status,
+    std::shared_ptr<NsmDevice> device)
+{
+    const std::string* requested = std::get_if<std::string>(&value);
+    if (!requested)
+    {
+        throw sdbusplus::error::xyz::openbmc_project::common::InvalidArgument{};
+    }
+
+    auto eid = device->getEid();
+    auto mode = TAVModeServer::convertTAVModeValueFromString(*requested);
+
+    uint8_t data = 0;
+    if (mode == TAVMode::Default)
+    {
+        data = NSM_TAV_MODE_DEFAULT;
+    }
+    else if (mode == TAVMode::Enabled)
+    {
+        data = NSM_TAV_MODE_ENABLED;
+    }
+    else if (mode == TAVMode::Disabled)
+    {
+        data = NSM_TAV_MODE_DISABLED;
+    }
+    else
+    {
+        throw sdbusplus::error::xyz::openbmc_project::common::InvalidArgument{};
+    }
+
+    if (!tavModeIntf->isModeConfigurable())
+    {
+        *status = AsyncOperationStatusType::Unavailable;
+        throw sdbusplus::error::xyz::openbmc_project::common::NotAllowed{};
+    }
+
+    Request request(sizeof(nsm_msg_hdr) +
+                    sizeof(nsm_set_device_mode_settings_v2_req) +
+                    TAV_MODE_DATA_SIZE - 1);
+    auto requestMsg = reinterpret_cast<nsm_msg*>(request.data());
+    auto rc = encode_set_device_mode_settings_v2_req(
+        0, DEVICE_MODE_TAV, &data, TAV_MODE_DATA_SIZE, requestMsg);
+    if (shouldLog("setTAVMode encode", uint16_t(0), uint8_t(0), rc))
+    {
+        lg2::error("Encoding TAV mode failed. eid={EID} rc={RC}", "EID", eid,
+                   "RC", rc);
+    }
+    if (rc)
+    {
+        *status = AsyncOperationStatusType::WriteFailure;
+        co_return NSM_SW_ERROR_COMMAND_FAIL;
+    }
+
+    std::shared_ptr<const nsm_msg> responseMsg;
+    size_t responseLen = 0;
+    auto rc_ = co_await device->postPatchIO(eid, request, responseMsg,
+                                            responseLen);
+    if (shouldLog("setTAVMode postPatchIO", uint16_t(0), uint8_t(0), rc_))
+    {
+        lg2::error("Setting TAV mode failed. eid={EID} rc={RC}", "EID", eid,
+                   "RC", utils::nsmSwCodeToString(rc_));
+    }
+    if (rc_)
+    {
+        *status = AsyncOperationStatusType::WriteFailure;
+        co_return NSM_SW_ERROR_COMMAND_FAIL;
+    }
+
+    uint8_t cc = NSM_SUCCESS;
+    uint16_t reason_code = ERR_NULL;
+    rc = decode_set_device_mode_settings_v2_resp(responseMsg.get(), responseLen,
+                                                 &cc, &reason_code);
+    if (shouldLog("setTAVMode response", reason_code, cc, rc))
+    {
+        lg2::error(
+            "Setting TAV mode returned an error. eid={EID} cc={CC} reasonCode={REASON} rc={RC}",
+            "EID", eid, "CC", cc, "REASON", reason_code, "RC", rc);
+    }
+    if (rc == NSM_SW_SUCCESS && cc == NSM_SUCCESS)
+    {
+        // Publish exactly what was requested, including Default -- a
+        // reset-to-default request is genuinely pending until the device
+        // reports a resolved value on a later poll. Do not guess Enabled.
+        tavModeIntf->pendingMode(mode);
+    }
+    else
+    {
+        *status = AsyncOperationStatusType::WriteFailure;
+        co_return NSM_SW_ERROR_COMMAND_FAIL;
+    }
+    co_return NSM_SW_SUCCESS;
+}
+
+inline void createNsmSwitchTAVMode(std::shared_ptr<NsmDevice> device,
+                                   sdbusplus::bus_t& bus,
+                                   const std::string& objPath,
+                                   const std::string& type,
+                                   const std::string& name)
+{
+    auto dbusObjPath = objPath + name + "/Oem/Nvidia/TAVMode";
+    std::vector<utils::Association> associations{
+        {"parent_switch", "tav_mode", objPath + name}};
+    auto tavModeAssociationIntf =
+        std::make_unique<AssociationDefinitionsInft>(bus, dbusObjPath.c_str());
+    std::vector<std::tuple<std::string, std::string, std::string>>
+        associationsList;
+    for (const auto& association : associations)
+    {
+        associationsList.emplace_back(association.forward, association.backward,
+                                      association.absolutePath);
+    }
+    tavModeAssociationIntf->associations(associationsList);
+
+    auto tavModeIntf = std::make_shared<TAVModeIntf>(bus, dbusObjPath.c_str());
+    tavModeIntf->currentMode(TAVMode::Enabled);
+    tavModeIntf->pendingMode(TAVMode::Enabled);
+    // Configurability is sourced solely from the SupportTAVMode
+    // entity-manager capability flag via the createNsmSwitchDI gate below.
+    tavModeIntf->isModeConfigurable(true);
+    auto nvSwitchTAVMode = std::make_shared<NsmSwitchTAVMode>(
+        name, type, tavModeIntf, std::move(tavModeAssociationIntf));
+    device->addSensor(nvSwitchTAVMode, false);
+
+    nsm::AsyncSetOperationHandler setTAVModeHandler = std::bind(
+        &NsmSwitchTAVMode::setTAVMode, nvSwitchTAVMode, std::placeholders::_1,
+        std::placeholders::_2, std::placeholders::_3);
+    AsyncOperationManager::getInstance()
+        ->getDispatcher(dbusObjPath)
+        ->addAsyncSetOperation(
+            std::string(TAVModeServer::interface), "PendingMode",
+            AsyncSetOperationInfo{setTAVModeHandler, nvSwitchTAVMode, device});
+}
+
 // Map NSM wire enum8 to LTXModeEnum. Wire Default (0) is resolved to Enabled
 // at D-Bus publish time per the LTX mode contract.
 static std::optional<LTXModeEnum> toLTXModeFromGet(uint8_t nsmMode)
@@ -1580,6 +1825,16 @@
                                             name);
         }
 
+        // An absent support property means TAV mode is not exposed. Read it
+        // from the cached base properties so absence needs no D-Bus fetch.
+        // dbusPropertyMapAsBool also normalizes an integer-encoded EM value.
+        const bool supportTAVMode =
+            dbusPropertyMapAsBool(allBaseIfaceProperties, "SupportTAVMode");
+        if (supportTAVMode)
+        {
+            createNsmSwitchTAVMode(device, bus, inventoryObjPath, type, name);
+        }
+
         bool supportLTXMode = false;
         auto ltxModeProperty = allBaseIfaceProperties.find("SupportLTXMode");
         if (ltxModeProperty != allBaseIfaceProperties.end())
diff --git a/nsmd/nsmDeviceInventory/nsmSwitch.hpp b/nsmd/nsmDeviceInventory/nsmSwitch.hpp
index 18c869b..42281cd 100644
--- a/nsmd/nsmDeviceInventory/nsmSwitch.hpp
+++ b/nsmd/nsmDeviceInventory/nsmSwitch.hpp
@@ -14,6 +14,7 @@
 
 #include <com/nvidia/DeviceMode/LTXMode/server.hpp>
 #include <com/nvidia/DeviceMode/PowerCappingMode/server.hpp>
+#include <com/nvidia/DeviceMode/TAVMode/server.hpp>
 #include <com/nvidia/DeviceMode/UPhyRecoveryMode/server.hpp>
 #include <com/nvidia/PowerMode/server.hpp>
 #include <com/nvidia/SwitchIsolation/server.hpp>
@@ -48,6 +49,13 @@
 using LTXModeServer = sdbusplus::com::nvidia::DeviceMode::server::LTXMode;
 using LTXModeIntf = object_t<LTXModeServer>;
 using LTXModeEnum = LTXModeServer::LinkTrainingExtendedMode;
+// TAVMode PDI shape: CurrentMode/PendingMode/IsModeConfigurable,
+// TAVModeValue{Default,Enabled,Disabled}. Gated on the PDI
+// com.nvidia.DeviceMode.TAVMode interface merging so this
+// generated header exists to compile against.
+using TAVModeServer = sdbusplus::com::nvidia::DeviceMode::server::TAVMode;
+using TAVModeIntf = object_t<TAVModeServer>;
+using TAVMode = TAVModeServer::TAVModeValue;
 using UPhyModeServer =
     sdbusplus::com::nvidia::DeviceMode::server::UPhyRecoveryMode;
 using UPhyModeIntf = object_t<UPhyModeServer>;
@@ -196,6 +204,37 @@
     std::shared_ptr<AssociationDefinitionsInft> associationDefIntf;
 };
 
+/** @brief TAV (Temperature Aware Voltage) mode using NSM Type 5 Device Mode
+ *         index 20 (DEVICE_MODE_TAV).
+ *
+ *         enum8 {0 Default, 1 Enabled, 2 Disabled}; 1-byte payload via the
+ *         generic v2 codec. */
+class NsmSwitchTAVMode : public NsmSensor
+{
+  public:
+    NsmSwitchTAVMode(
+        const std::string& name, const std::string& type,
+        std::shared_ptr<TAVModeIntf> tavModeIntf,
+        std::shared_ptr<AssociationDefinitionsInft> associationDefIntf) :
+        NsmSensor(name, type), tavModeIntf(tavModeIntf),
+        associationDefIntf(associationDefIntf)
+    {}
+
+    std::optional<std::vector<uint8_t>>
+        genRequestMsg(eid_t eid, uint8_t instanceId) override;
+    uint8_t handleResponseMsg(const struct nsm_msg* responseMsg,
+                              size_t responseLen) override;
+
+    requester::Coroutine
+        setTAVMode(const AsyncSetOperationValueType& value,
+                   [[maybe_unused]] AsyncOperationStatusType* status,
+                   std::shared_ptr<NsmDevice> device);
+
+  private:
+    std::shared_ptr<TAVModeIntf> tavModeIntf;
+    std::shared_ptr<AssociationDefinitionsInft> associationDefIntf;
+};
+
 /** @brief LTX (Link Training Extended) mode using NSM Type 5 Device Mode
  *         index 29 (DEVICE_MODE_LTX). */
 class NsmSwitchLTXMode : public NsmSensor
diff --git a/nsmd/nsmDeviceInventory/test/nsmSwitchFactoryBranch_test.cpp b/nsmd/nsmDeviceInventory/test/nsmSwitchFactoryBranch_test.cpp
index cda22b3..ace8a8f 100644
--- a/nsmd/nsmDeviceInventory/test/nsmSwitchFactoryBranch_test.cpp
+++ b/nsmd/nsmDeviceInventory/test/nsmSwitchFactoryBranch_test.cpp
@@ -1234,3 +1234,271 @@
     rc = sensor.handleResponseMsg(response, responseMsg.size());
     EXPECT_EQ(rc, NSM_SW_ERROR_LENGTH);
 }
+
+// ============================================================================
+// NsmSwitchTAVMode -- NSM Type 5 Device Mode index 20 coverage.
+// ============================================================================
+
+TEST_F(NsmSwitchFactoryBranchTest, TAVMode_GenRequestMsg_EncodeFail)
+{
+    static auto& testBus = utils::DBusHandler::getBus();
+    auto tavIntf = std::make_shared<TAVModeIntf>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_genreq_fail");
+    auto assocIntf = std::make_shared<AssociationDefinitionsInft>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_genreq_fail");
+    NsmSwitchTAVMode sensor("Tav_genreq", "NSM_NVSwitch", tavIntf, assocIntf);
+
+    auto request = sensor.genRequestMsg(12, NSM_INSTANCE_MAX + 1);
+    EXPECT_FALSE(request.has_value());
+}
+
+TEST_F(NsmSwitchFactoryBranchTest, TAVMode_HandleResponse_AllModes)
+{
+    static auto& testBus = utils::DBusHandler::getBus();
+    auto tavIntf = std::make_shared<TAVModeIntf>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_all_modes");
+    auto assocIntf = std::make_shared<AssociationDefinitionsInft>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_all_modes");
+    NsmSwitchTAVMode sensor("Tav_modes", "NSM_NVSwitch", tavIntf, assocIntf);
+
+    std::vector<uint8_t> responseMsg(
+        sizeof(nsm_msg_hdr) + sizeof(nsm_get_device_mode_settings_v2_resp) +
+            TAV_MODE_DATA_SIZE * 2,
+        0);
+    auto response = reinterpret_cast<nsm_msg*>(responseMsg.data());
+
+    uint8_t currentMode = NSM_TAV_MODE_ENABLED;
+    uint8_t pendingMode = NSM_TAV_MODE_DISABLED;
+    auto rc = encode_get_device_mode_settings_v2_resp(
+        0, NSM_SUCCESS, ERR_NULL, &currentMode, TAV_MODE_DATA_SIZE,
+        &pendingMode, TAV_MODE_DATA_SIZE, response);
+    ASSERT_EQ(rc, NSM_SW_SUCCESS);
+
+    rc = sensor.handleResponseMsg(response, responseMsg.size());
+    EXPECT_EQ(rc, NSM_SW_SUCCESS);
+    EXPECT_EQ(tavIntf->currentMode(), TAVMode::Enabled);
+    EXPECT_EQ(tavIntf->pendingMode(), TAVMode::Disabled);
+}
+
+TEST_F(NsmSwitchFactoryBranchTest,
+       TAVMode_HandleResponse_DefaultWirePassedThrough)
+{
+    static auto& testBus = utils::DBusHandler::getBus();
+    auto tavIntf = std::make_shared<TAVModeIntf>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_default_wire");
+    auto assocIntf = std::make_shared<AssociationDefinitionsInft>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_default_wire");
+    NsmSwitchTAVMode sensor("Tav_defwire", "NSM_NVSwitch", tavIntf, assocIntf);
+
+    std::vector<uint8_t> responseMsg(
+        sizeof(nsm_msg_hdr) + sizeof(nsm_get_device_mode_settings_v2_resp) +
+            TAV_MODE_DATA_SIZE * 2,
+        0);
+    auto response = reinterpret_cast<nsm_msg*>(responseMsg.data());
+
+    uint8_t currentMode = NSM_TAV_MODE_DEFAULT;
+    uint8_t pendingMode = NSM_TAV_MODE_DEFAULT;
+    auto rc = encode_get_device_mode_settings_v2_resp(
+        0, NSM_SUCCESS, ERR_NULL, &currentMode, TAV_MODE_DATA_SIZE,
+        &pendingMode, TAV_MODE_DATA_SIZE, response);
+    ASSERT_EQ(rc, NSM_SW_SUCCESS);
+
+    rc = sensor.handleResponseMsg(response, responseMsg.size());
+    EXPECT_EQ(rc, NSM_SW_SUCCESS);
+    // nsmd never resolves Default -- it publishes the wire value as-is and
+    // leaves resolution to the device / to bmcweb's not-yet-resolved
+    // handling (skip on Settings, error on the active resource).
+    EXPECT_EQ(tavIntf->currentMode(), TAVMode::Default);
+    EXPECT_EQ(tavIntf->pendingMode(), TAVMode::Default);
+}
+
+TEST_F(NsmSwitchFactoryBranchTest, TAVMode_HandleResponse_InvalidMode)
+{
+    static auto& testBus = utils::DBusHandler::getBus();
+    auto tavIntf = std::make_shared<TAVModeIntf>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_invalid_mode");
+    auto assocIntf = std::make_shared<AssociationDefinitionsInft>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_invalid_mode");
+    NsmSwitchTAVMode sensor("Tav_invalid", "NSM_NVSwitch", tavIntf, assocIntf);
+
+    std::vector<uint8_t> responseMsg(
+        sizeof(nsm_msg_hdr) + sizeof(nsm_get_device_mode_settings_v2_resp) +
+            TAV_MODE_DATA_SIZE,
+        0);
+    auto response = reinterpret_cast<nsm_msg*>(responseMsg.data());
+
+    uint8_t currentMode = 99;
+    auto rc = encode_get_device_mode_settings_v2_resp(
+        0, NSM_SUCCESS, ERR_NULL, &currentMode, TAV_MODE_DATA_SIZE, nullptr, 0,
+        response);
+    ASSERT_EQ(rc, NSM_SW_SUCCESS);
+
+    rc = sensor.handleResponseMsg(response, responseMsg.size());
+    EXPECT_EQ(rc, NSM_SW_ERROR_DATA);
+}
+
+TEST_F(NsmSwitchFactoryBranchTest, SetTAVMode_NotConfigurable_NotAllowed)
+{
+    static auto& testBus = utils::DBusHandler::getBus();
+    auto tavIntf = std::make_shared<TAVModeIntf>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_not_cfg");
+    auto assocIntf = std::make_shared<AssociationDefinitionsInft>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_not_cfg");
+    tavIntf->isModeConfigurable(false);
+    NsmSwitchTAVMode sensor("Tav_nocfg", "NSM_NVSwitch", tavIntf, assocIntf);
+
+    AsyncOperationStatusType status = AsyncOperationStatusType::Success;
+    AsyncSetOperationValueType value =
+        TAVModeServer::convertTAVModeValueToString(TAVMode::Default);
+
+    EXPECT_THROW_COROUTINE(
+        sensor.setTAVMode(value, &status, nvswitch),
+        sdbusplus::error::xyz::openbmc_project::common::NotAllowed);
+    EXPECT_EQ(status, AsyncOperationStatusType::Unavailable);
+}
+
+TEST_F(NsmSwitchFactoryBranchTest, SetTAVMode_Default_Success)
+{
+    static auto& testBus = utils::DBusHandler::getBus();
+    auto tavIntf = std::make_shared<TAVModeIntf>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_set_default");
+    auto assocIntf = std::make_shared<AssociationDefinitionsInft>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_set_default");
+    // The factory publishes IsModeConfigurable true; the PDI default is false.
+    tavIntf->isModeConfigurable(true);
+    NsmSwitchTAVMode sensor("Tav_setdef", "NSM_NVSwitch", tavIntf, assocIntf);
+
+    AsyncOperationStatusType status = AsyncOperationStatusType::Success;
+    AsyncSetOperationValueType value =
+        TAVModeServer::convertTAVModeValueToString(TAVMode::Default);
+
+    std::vector<uint8_t> responseData(
+        sizeof(nsm_msg_hdr) + sizeof(nsm_common_resp), 0);
+    auto* responseMsg = reinterpret_cast<nsm_msg*>(responseData.data());
+    encode_set_device_mode_settings_v2_resp(0, NSM_SUCCESS, ERR_NULL,
+                                            responseMsg);
+
+    EXPECT_CALL(*nvswitch, postPatchIO(_, _, _, _))
+        .WillOnce(mockPostPatchIO(responseData));
+
+    sensor.setTAVMode(value, &status, nvswitch);
+
+    EXPECT_EQ(status, AsyncOperationStatusType::Success);
+    // A reset-to-default request publishes Default on PendingMode -- it is
+    // genuinely pending until the device reports a resolved value on a
+    // later poll; nsmd does not guess Enabled.
+    EXPECT_EQ(tavIntf->pendingMode(), TAVMode::Default);
+}
+
+// ============================================================================
+// createNsmSwitchDI gate: SupportTAVMode true/false
+// ============================================================================
+
+static size_t countTAVModeSensors(const std::shared_ptr<MockNsmDevice>& dev)
+{
+    size_t count = 0;
+    for (const auto& sensor : dev->roundRobinSensors)
+    {
+        if (std::dynamic_pointer_cast<NsmSwitchTAVMode>(sensor))
+        {
+            ++count;
+        }
+    }
+    return count;
+}
+
+TEST_F(NsmSwitchFactoryBranchTest, Factory_NVSwitch_SupportTAVModeTrue)
+{
+    const std::string path = inventoryPath + "nvs_tav_true";
+    setupBaseProperties(path, {{"Type", std::string("NSM_NVSwitch")},
+                               {"SupportL1PredictionMode", bool(false)},
+                               {"SupportTAVMode", bool(true)}});
+
+    ASSERT_EQ(countTAVModeSensors(nvswitch), 0u);
+    createNsmSwitchDI(mockManager, baseIntf, path);
+    EXPECT_EQ(countTAVModeSensors(nvswitch), 1u);
+}
+
+TEST_F(NsmSwitchFactoryBranchTest, Factory_NVSwitch_SupportTAVModeFalse)
+{
+    const std::string path = inventoryPath + "nvs_tav_false";
+    setupBaseProperties(path, {{"Type", std::string("NSM_NVSwitch")},
+                               {"SupportL1PredictionMode", bool(false)},
+                               {"SupportTAVMode", bool(false)}});
+
+    const size_t before = nvswitch->roundRobinSensors.size();
+    createNsmSwitchDI(mockManager, baseIntf, path);
+    // Gate closed for TAV mode, other NVSwitch sensors still created.
+    EXPECT_EQ(countTAVModeSensors(nvswitch), 0u);
+    EXPECT_GT(nvswitch->roundRobinSensors.size(), before);
+}
+
+TEST_F(NsmSwitchFactoryBranchTest, Factory_NVSwitch_SupportTAVModeAbsent)
+{
+    const std::string path = inventoryPath + "nvs_tav_absent";
+    // SupportTAVMode omitted entirely from the base properties.
+    setupBaseProperties(path, {{"Type", std::string("NSM_NVSwitch")},
+                               {"SupportL1PredictionMode", bool(false)}});
+
+    const size_t before = nvswitch->roundRobinSensors.size();
+    createNsmSwitchDI(mockManager, baseIntf, path);
+    EXPECT_EQ(countTAVModeSensors(nvswitch), 0u);
+    EXPECT_GT(nvswitch->roundRobinSensors.size(), before);
+}
+
+TEST_F(NsmSwitchFactoryBranchTest, TAVMode_HandleResponse_NoPendingSyncsPending)
+{
+    static auto& testBus = utils::DBusHandler::getBus();
+    auto tavIntf = std::make_shared<TAVModeIntf>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_no_pending");
+    auto assocIntf = std::make_shared<AssociationDefinitionsInft>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_no_pending");
+    // Stale pending from a prior write / factory seed.
+    tavIntf->pendingMode(TAVMode::Disabled);
+    NsmSwitchTAVMode sensor("Tav_nopending", "NSM_NVSwitch", tavIntf,
+                            assocIntf);
+
+    std::vector<uint8_t> responseMsg(
+        sizeof(nsm_msg_hdr) + sizeof(nsm_get_device_mode_settings_v2_resp) +
+            TAV_MODE_DATA_SIZE,
+        0);
+    auto response = reinterpret_cast<nsm_msg*>(responseMsg.data());
+
+    uint8_t currentMode = NSM_TAV_MODE_ENABLED;
+    auto rc = encode_get_device_mode_settings_v2_resp(
+        0, NSM_SUCCESS, ERR_NULL, &currentMode, TAV_MODE_DATA_SIZE, nullptr, 0,
+        response);
+    ASSERT_EQ(rc, NSM_SW_SUCCESS);
+
+    rc = sensor.handleResponseMsg(response, responseMsg.size());
+    EXPECT_EQ(rc, NSM_SW_SUCCESS);
+    EXPECT_EQ(tavIntf->currentMode(), TAVMode::Enabled);
+    EXPECT_EQ(tavIntf->pendingMode(), TAVMode::Enabled);
+}
+
+TEST_F(NsmSwitchFactoryBranchTest, TAVMode_HandleResponse_BadLength)
+{
+    static auto& testBus = utils::DBusHandler::getBus();
+    auto tavIntf = std::make_shared<TAVModeIntf>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_bad_len");
+    auto assocIntf = std::make_shared<AssociationDefinitionsInft>(
+        testBus, "/xyz/openbmc_project/inventory/fabr/tav_bad_len");
+    NsmSwitchTAVMode sensor("Tav_badlen", "NSM_NVSwitch", tavIntf, assocIntf);
+
+    std::vector<uint8_t> responseMsg(
+        sizeof(nsm_msg_hdr) + sizeof(nsm_get_device_mode_settings_v2_resp) + 4,
+        0);
+    auto response = reinterpret_cast<nsm_msg*>(responseMsg.data());
+
+    // Two source bytes so the encoder stays in bounds while still emitting a
+    // 2-byte current payload, which is malformed for enum8 index 20.
+    uint8_t currentMode[2] = {NSM_TAV_MODE_ENABLED, 0};
+    uint8_t pendingMode = NSM_TAV_MODE_DISABLED;
+    auto rc = encode_get_device_mode_settings_v2_resp(
+        0, NSM_SUCCESS, ERR_NULL, currentMode, uint16_t(sizeof(currentMode)),
+        &pendingMode, TAV_MODE_DATA_SIZE, response);
+    ASSERT_EQ(rc, NSM_SW_SUCCESS);
+
+    rc = sensor.handleResponseMsg(response, responseMsg.size());
+    EXPECT_EQ(rc, NSM_SW_ERROR_LENGTH);
+}
diff --git a/nsmtool/nsm_config_cmd.cpp b/nsmtool/nsm_config_cmd.cpp
index 378641e..994bb68 100644
--- a/nsmtool/nsm_config_cmd.cpp
+++ b/nsmtool/nsm_config_cmd.cpp
@@ -2258,6 +2258,195 @@
 };
 
 /**
+ * Human-readable NSM Type 5 Device Mode Index 20 (TAV Mode) enum8.
+ */
+namespace
+{
+inline std::string tavModeFromGetWireToString(uint8_t value)
+{
+    switch (value)
+    {
+        case NSM_TAV_MODE_ENABLED:
+            return "Enabled";
+        case NSM_TAV_MODE_DISABLED:
+            return "Disabled";
+        case NSM_TAV_MODE_DEFAULT:
+            return "Default";
+        default:
+            return "Unknown(" + std::to_string(static_cast<int>(value)) + ")";
+    }
+}
+} // namespace
+
+/**
+ * config subcommands targeting NSM Type 5 DEVICE_MODE_TAV (index 20)
+ * so the TAV mode (NSM Type 5 idx 20) enable/disable mode can be read/set
+ * without hand-rolling a raw Get/Set Device Mode Settings v2 command.
+ */
+class GetTAVMode : public CommandInterface
+{
+  public:
+    ~GetTAVMode() = default;
+    GetTAVMode() = delete;
+    GetTAVMode(const GetTAVMode&) = delete;
+    GetTAVMode(GetTAVMode&&) = default;
+    GetTAVMode& operator=(const GetTAVMode&) = delete;
+    GetTAVMode& operator=(GetTAVMode&&) = default;
+
+    using CommandInterface::CommandInterface;
+
+    explicit GetTAVMode(const char* type, const char* name, CLI::App* app) :
+        CommandInterface(type, name, app)
+    {}
+
+    std::pair<int, std::vector<uint8_t>> createRequestMsg() override
+    {
+        std::vector<uint8_t> requestMsg(
+            sizeof(nsm_msg_hdr) + sizeof(nsm_get_device_mode_settings_v2_req),
+            0);
+        auto request = reinterpret_cast<nsm_msg*>(requestMsg.data());
+        auto rc = encode_get_device_mode_settings_v2_req(
+            instanceId, static_cast<uint32_t>(DEVICE_MODE_TAV), request);
+        return {rc, requestMsg};
+    }
+
+    void parseResponseMsg(nsm_msg* responsePtr, size_t payloadLength) override
+    {
+        uint8_t cc = NSM_ERROR;
+        uint16_t reasonCode = ERR_NULL;
+        uint8_t currentData[TAV_MODE_DATA_SIZE] = {};
+        uint8_t pendingData[TAV_MODE_DATA_SIZE] = {};
+        uint16_t currentLength = 0;
+        uint16_t pendingLength = 0;
+
+        /* Probe lengths first: reject oversized mode payloads before decode
+         * copies into the fixed TAV_MODE_DATA_SIZE stack buffers. */
+        auto rc = decode_get_device_mode_settings_v2_resp(
+            responsePtr, payloadLength, &cc, &reasonCode, nullptr,
+            &currentLength, nullptr, &pendingLength);
+        if (rc != NSM_SW_SUCCESS || cc != NSM_SUCCESS)
+        {
+            std::cerr << "Response message error: rc=" << rc
+                      << ", cc=" << static_cast<int>(cc)
+                      << ", reasonCode=" << static_cast<int>(reasonCode)
+                      << "\n";
+            return;
+        }
+        if (currentLength != TAV_MODE_DATA_SIZE ||
+            (pendingLength != 0 && pendingLength != TAV_MODE_DATA_SIZE))
+        {
+            std::cerr << "TAV mode payload invalid: current=" << currentLength
+                      << " pending=" << pendingLength
+                      << " (expected current==" << TAV_MODE_DATA_SIZE
+                      << ", pending==0 or " << TAV_MODE_DATA_SIZE << ")\n";
+            return;
+        }
+        rc = decode_get_device_mode_settings_v2_resp(
+            responsePtr, payloadLength, &cc, &reasonCode, currentData,
+            &currentLength, pendingData, &pendingLength);
+        if (rc != NSM_SW_SUCCESS || cc != NSM_SUCCESS)
+        {
+            std::cerr << "Response message error: rc=" << rc
+                      << ", cc=" << static_cast<int>(cc)
+                      << ", reasonCode=" << static_cast<int>(reasonCode)
+                      << "\n";
+            return;
+        }
+        ordered_json result;
+        result["Completion Code"] = cc;
+        result["CurrentMode"] = tavModeFromGetWireToString(currentData[0]);
+        if (pendingLength == TAV_MODE_DATA_SIZE)
+        {
+            result["PendingMode"] = tavModeFromGetWireToString(pendingData[0]);
+            result["ResetRequired"] = (pendingData[0] != currentData[0]);
+        }
+        nsmtool::helper::DisplayInJson(result);
+    }
+};
+
+class SetTAVMode : public CommandInterface
+{
+  public:
+    ~SetTAVMode() = default;
+    SetTAVMode() = delete;
+    SetTAVMode(const SetTAVMode&) = delete;
+    SetTAVMode(SetTAVMode&&) = default;
+    SetTAVMode& operator=(const SetTAVMode&) = delete;
+    SetTAVMode& operator=(SetTAVMode&&) = default;
+
+    using CommandInterface::CommandInterface;
+
+    explicit SetTAVMode(const char* type, const char* name, CLI::App* app) :
+        CommandInterface(type, name, app)
+    {
+        auto g = app->add_option_group(
+            "Required",
+            "Set TAV Mode (NSM Type 5 idx 20): Default | Enabled | Disabled");
+        g->add_option("-M, --mode", modeStr,
+                      "TAV mode: Default, Enabled, or Disabled");
+        g->require_option(1);
+    }
+
+    std::pair<int, std::vector<uint8_t>> createRequestMsg() override
+    {
+        uint8_t mode = 0;
+        std::string m = modeStr;
+        std::transform(m.begin(), m.end(), m.begin(),
+                       [](unsigned char c) { return std::tolower(c); });
+        if (m == "default" || m == "0")
+        {
+            mode = NSM_TAV_MODE_DEFAULT;
+        }
+        else if (m == "enabled" || m == "enable" || m == "1")
+        {
+            mode = NSM_TAV_MODE_ENABLED;
+        }
+        else if (m == "disabled" || m == "disable" || m == "2")
+        {
+            mode = NSM_TAV_MODE_DISABLED;
+        }
+        else
+        {
+            std::cerr << "Invalid mode '" << modeStr
+                      << "' (expected Default, Enabled, or Disabled)\n";
+            return {NSM_SW_ERROR_DATA, {}};
+        }
+
+        std::vector<uint8_t> requestMsg(
+            sizeof(nsm_msg_hdr) + sizeof(nsm_set_device_mode_settings_v2_req) +
+                TAV_MODE_DATA_SIZE - 1,
+            0);
+        auto request = reinterpret_cast<nsm_msg*>(requestMsg.data());
+        auto rc = encode_set_device_mode_settings_v2_req(
+            instanceId, static_cast<uint32_t>(DEVICE_MODE_TAV), &mode,
+            TAV_MODE_DATA_SIZE, request);
+        return {rc, requestMsg};
+    }
+
+    void parseResponseMsg(nsm_msg* responsePtr, size_t payloadLength) override
+    {
+        uint8_t cc = NSM_ERROR;
+        uint16_t reasonCode = ERR_NULL;
+        auto rc = decode_set_device_mode_settings_v2_resp(
+            responsePtr, payloadLength, &cc, &reasonCode);
+        if (rc != NSM_SW_SUCCESS || cc != NSM_SUCCESS)
+        {
+            std::cerr << "Response error: rc=" << rc
+                      << ", cc=" << static_cast<int>(cc)
+                      << ", reasonCode=" << static_cast<int>(reasonCode)
+                      << "\n";
+            return;
+        }
+        ordered_json result;
+        result["Completion Code"] = cc;
+        nsmtool::helper::DisplayInJson(result);
+    }
+
+  private:
+    std::string modeStr{};
+};
+
+/**
  * config subcommands targeting NSM Type 5 DEVICE_MODE_LTX (index 29)
  * so the LTX enable/disable mode can be read/set without hand-rolling a raw
  * Get/Set Device Mode Settings v2 command.
@@ -2765,6 +2954,16 @@
     commands.push_back(std::make_unique<SetPowerCappingMode>(
         "config", "SetPowerCappingMode", setPowerCappingMode));
 
+    auto getTAVMode = config->add_subcommand(
+        "GetTAVMode", "Get TAV mode (NSM Type 5 idx 20)");
+    commands.push_back(
+        std::make_unique<GetTAVMode>("config", "GetTAVMode", getTAVMode));
+
+    auto setTAVMode = config->add_subcommand(
+        "SetTAVMode", "Set TAV mode (NSM Type 5 idx 20)");
+    commands.push_back(
+        std::make_unique<SetTAVMode>("config", "SetTAVMode", setTAVMode));
+
     auto getLTXMode = config->add_subcommand(
         "GetLTXMode", "Get LTX mode (NSM Type 5 idx DEVICE_MODE_LTX)");
     commands.push_back(
diff --git a/nsmtool/test/nsm_config_cmd_branch_test.cpp b/nsmtool/test/nsm_config_cmd_branch_test.cpp
index e138967..81bf083 100644
--- a/nsmtool/test/nsm_config_cmd_branch_test.cpp
+++ b/nsmtool/test/nsm_config_cmd_branch_test.cpp
@@ -588,7 +588,7 @@
     ASSERT_EQ(rc, NSM_SW_SUCCESS);
 
     testing::internal::CaptureStdout();
-    EXPECT_NO_THROW(commands[24]->parseResponseMsg(msg, buf.size()));
+    EXPECT_NO_THROW(commands[26]->parseResponseMsg(msg, buf.size()));
     std::string output = testing::internal::GetCapturedStdout();
     EXPECT_NE(output.find("CurrentMode"), std::string::npos);
     EXPECT_NE(output.find("PendingMode"), std::string::npos);
@@ -614,7 +614,7 @@
 
     testing::internal::CaptureStdout();
     testing::internal::CaptureStderr();
-    EXPECT_NO_THROW(commands[24]->parseResponseMsg(msg, buf.size()));
+    EXPECT_NO_THROW(commands[26]->parseResponseMsg(msg, buf.size()));
     std::string stdoutput = testing::internal::GetCapturedStdout();
     std::string stderrOutput = testing::internal::GetCapturedStderr();
     // No partial-success JSON on stdout ...
@@ -629,13 +629,13 @@
     setupConfigCommands(app);
     parseSubcmdArgs(app, "SetLTXMode", {"-M", "Enabled"});
 
-    auto [rc, reqMsg] = commands[25]->createRequestMsg();
+    auto [rc, reqMsg] = commands[27]->createRequestMsg();
     ASSERT_EQ(rc, NSM_SW_SUCCESS);
 
     std::vector<uint8_t> buf(sizeof(nsm_msg_hdr) + sizeof(nsm_common_resp));
     auto* msg = reinterpret_cast<nsm_msg*>(buf.data());
     encode_set_device_mode_settings_v2_resp(0, NSM_SUCCESS, ERR_NULL, msg);
-    EXPECT_NO_THROW(commands[25]->parseResponseMsg(msg, buf.size()));
+    EXPECT_NO_THROW(commands[27]->parseResponseMsg(msg, buf.size()));
 }
 
 TEST(ConfigBranch, SetLTXMode_InvalidMode_ReturnsError)
@@ -644,7 +644,7 @@
     setupConfigCommands(app);
     parseSubcmdArgs(app, "SetLTXMode", {"-M", "NotAMode"});
 
-    auto [rc, reqMsg] = commands[25]->createRequestMsg();
+    auto [rc, reqMsg] = commands[27]->createRequestMsg();
     EXPECT_NE(rc, NSM_SW_SUCCESS);
     EXPECT_TRUE(reqMsg.empty());
 }
@@ -672,7 +672,7 @@
     ASSERT_EQ(rc, NSM_SW_SUCCESS);
 
     testing::internal::CaptureStdout();
-    EXPECT_NO_THROW(commands[26]->parseResponseMsg(msg, buf.size()));
+    EXPECT_NO_THROW(commands[28]->parseResponseMsg(msg, buf.size()));
     std::string output = testing::internal::GetCapturedStdout();
     EXPECT_NE(output.find("CurrentMode"), std::string::npos);
     EXPECT_NE(output.find("PendingMode"), std::string::npos);
@@ -697,7 +697,7 @@
 
     testing::internal::CaptureStdout();
     testing::internal::CaptureStderr();
-    EXPECT_NO_THROW(commands[26]->parseResponseMsg(msg, buf.size()));
+    EXPECT_NO_THROW(commands[28]->parseResponseMsg(msg, buf.size()));
     std::string stdoutput = testing::internal::GetCapturedStdout();
     std::string stderrOutput = testing::internal::GetCapturedStderr();
     EXPECT_TRUE(stdoutput.empty());
@@ -711,13 +711,13 @@
     setupConfigCommands(app);
     parseSubcmdArgs(app, "SetUPhyMode", {"-M", "Enabled"});
 
-    auto [rc, reqMsg] = commands[27]->createRequestMsg();
+    auto [rc, reqMsg] = commands[29]->createRequestMsg();
     ASSERT_EQ(rc, NSM_SW_SUCCESS);
 
     std::vector<uint8_t> buf(sizeof(nsm_msg_hdr) + sizeof(nsm_common_resp));
     auto* msg = reinterpret_cast<nsm_msg*>(buf.data());
     encode_set_device_mode_settings_v2_resp(0, NSM_SUCCESS, ERR_NULL, msg);
-    EXPECT_NO_THROW(commands[27]->parseResponseMsg(msg, buf.size()));
+    EXPECT_NO_THROW(commands[29]->parseResponseMsg(msg, buf.size()));
 }
 
 TEST(ConfigBranch, SetUPhyMode_InvalidMode_ReturnsError)
@@ -726,7 +726,7 @@
     setupConfigCommands(app);
     parseSubcmdArgs(app, "SetUPhyMode", {"-M", "NotAMode"});
 
-    auto [rc, reqMsg] = commands[27]->createRequestMsg();
+    auto [rc, reqMsg] = commands[29]->createRequestMsg();
     EXPECT_NE(rc, NSM_SW_SUCCESS);
     EXPECT_TRUE(reqMsg.empty());
 }