diff --git a/common/rdm/DummyResponder.cpp b/common/rdm/DummyResponder.cpp
index 8449b1226..20241e7d3 100644
--- a/common/rdm/DummyResponder.cpp
+++ b/common/rdm/DummyResponder.cpp
@@ -172,6 +172,15 @@ const ResponderOps<DummyResponder>::ParamHandler
   { PID_TEST_DATA,
     &DummyResponder::GetTestData,
     &DummyResponder::SetTestData},
+  { PID_METADATA_PARAMETER_VERSION,
+    &DummyResponder::GetMetadataParameterVersion,
+    NULL},
+  { PID_METADATA_JSON,
+    &DummyResponder::GetMetadataJSON,
+    NULL},
+  { PID_METADATA_JSON_URL,
+    &DummyResponder::GetMetadataJSONURL,
+    NULL},
   { OLA_MANUFACTURER_PID_CODE_VERSION,
     &DummyResponder::GetOlaCodeVersion,
     NULL},
@@ -460,7 +469,7 @@ RDMResponse *DummyResponder::GetProductURL(
       request,
       "https://openlighting.org/rdm-tools/dummy-responders/",
       0,
-      UINT8_MAX); // TODO(Peter): This field's length isn't limited in the spec
+      UINT8_MAX);  // TODO(Peter): This field's length isn't limited in the spec
 }
 
 RDMResponse *DummyResponder::GetFirmwareURL(
@@ -469,7 +478,7 @@ RDMResponse *DummyResponder::GetFirmwareURL(
       request,
       "https://github.com/OpenLightingProject/ola",
       0,
-      UINT8_MAX); // TODO(Peter): This field's length isn't limited in the spec
+      UINT8_MAX);  // TODO(Peter): This field's length isn't limited in the spec
 }
 
 RDMResponse *DummyResponder::GetTestData(const RDMRequest *request) {
@@ -480,6 +489,58 @@ RDMResponse *DummyResponder::SetTestData(const RDMRequest *request) {
   return ResponderHelper::SetTestData(request);
 }
 
+RDMResponse *DummyResponder::GetMetadataParameterVersion(
+    const RDMRequest *request) {
+  // Check that it's OLA_MANUFACTURER_PID_CODE_VERSION being requested
+  uint16_t parameter_id;
+  if (!ResponderHelper::ExtractUInt16(request, &parameter_id)) {
+    return NackWithReason(request, NR_FORMAT_ERROR);
+  }
+
+  if (parameter_id != OLA_MANUFACTURER_PID_CODE_VERSION) {
+    OLA_WARN << "Dummy responder received metadata parameter version request "
+             << "with unknown PID, expected "
+             << OLA_MANUFACTURER_PID_CODE_VERSION << ", got " << parameter_id;
+    return NackWithReason(request, NR_DATA_OUT_OF_RANGE);
+  } else {
+    return ResponderHelper::GetMetadataParameterVersion(
+        request,
+        OLA_MANUFACTURER_PID_CODE_VERSION,
+        OLA_MANUFACTURER_PID_JSON_VERSION_CODE_VERSION);
+  }
+}
+
+RDMResponse *DummyResponder::GetMetadataJSON(
+    const RDMRequest *request) {
+  // Check that it's OLA_MANUFACTURER_PID_CODE_VERSION being requested
+  uint16_t parameter_id;
+  if (!ResponderHelper::ExtractUInt16(request, &parameter_id)) {
+    return NackWithReason(request, NR_FORMAT_ERROR);
+  }
+
+  if (parameter_id != OLA_MANUFACTURER_PID_CODE_VERSION) {
+    OLA_WARN << "Dummy responder received metadata JSON request with unknown "
+             << "PID, expected "
+             << OLA_MANUFACTURER_PID_CODE_VERSION << ", got " << parameter_id;
+    return NackWithReason(request, NR_DATA_OUT_OF_RANGE);
+  } else {
+    return ResponderHelper::GetMetadataJSON(
+        request,
+        OLA_MANUFACTURER_PID_CODE_VERSION,
+        OLA_MANUFACTURER_PID_JSON_CODE_VERSION);
+  }
+}
+
+RDMResponse *DummyResponder::GetMetadataJSONURL(
+    const RDMRequest *request) {
+  return ResponderHelper::GetString(
+      request,
+      // TODO(Peter): Consider what this should actually be permanently
+      "https://docs.openlighting.org/ola/json/latest/metadata/0x0001.json",
+      0,
+      UINT8_MAX);  // TODO(Peter): This field's length isn't limited in the spec
+}
+
 RDMResponse *DummyResponder::GetOlaCodeVersion(
     const RDMRequest *request) {
   return ResponderHelper::GetString(request, VERSION);
diff --git a/common/rdm/OpenLightingEnums.cpp b/common/rdm/OpenLightingEnums.cpp
index a2fca2377..08b118780 100644
--- a/common/rdm/OpenLightingEnums.cpp
+++ b/common/rdm/OpenLightingEnums.cpp
@@ -24,5 +24,11 @@ namespace rdm {
 
 const char OLA_MANUFACTURER_LABEL[] = "Open Lighting Project";
 const char OLA_MANUFACTURER_URL[] = "https://openlighting.org/";
+
+const char OLA_MANUFACTURER_PID_JSON_CODE_VERSION[] = "{\"name\":"
+    "\"CODE_VERSION\",\"manufacturer_id\":31344,\"pid\":32769,\"version\":1,"
+    "\"get_request_subdevice_range\":[\"root\",\"subdevices\"],"
+    "\"get_request\":[],\"get_response\":[{\"name\":\"code_version\","
+    "\"type\":\"string\",""\"maxLength\":32,\"restrictToASCII\":true}]}";
 }  // namespace rdm
 }  // namespace ola
diff --git a/common/rdm/ResponderHelper.cpp b/common/rdm/ResponderHelper.cpp
index b4528f3c2..09b461f9a 100644
--- a/common/rdm/ResponderHelper.cpp
+++ b/common/rdm/ResponderHelper.cpp
@@ -965,7 +965,7 @@ RDMResponse *ResponderHelper::GetParamDescription(
     uint32_t min_value,
     uint32_t default_value,
     uint32_t max_value,
-    string description,
+    const string description,
     uint8_t queued_message_count) {
   PACK(
   struct parameter_description_s {
@@ -1019,7 +1019,7 @@ RDMResponse *ResponderHelper::GetASCIIParamDescription(
         const RDMRequest *request,
         uint16_t pid,
         rdm_command_class command_class,
-        string description,
+        const string description,
         uint8_t queued_message_count) {
   return GetParamDescription(
       request,
@@ -1041,7 +1041,7 @@ RDMResponse *ResponderHelper::GetBitFieldParamDescription(
         uint16_t pid,
         uint8_t pdl_size,
         rdm_command_class command_class,
-        string description,
+        const string description,
         uint8_t queued_message_count) {
   return GetParamDescription(
       request,
@@ -1110,6 +1110,65 @@ RDMResponse *ResponderHelper::SetTestData(
       queued_message_count);
 }
 
+/**
+ * Get NSC comms status
+ */
+RDMResponse *ResponderHelper::GetCommsStatusNSC(
+    const RDMRequest *request,
+    const NSCStatus *status,
+    uint8_t queued_message_count) {
+  if (request->ParamDataSize()) {
+    return NackWithReason(request, NR_FORMAT_ERROR, queued_message_count);
+  }
+
+  PACK(
+  struct comms_status_nsc_s {
+    uint8_t supported_fields;
+    uint32_t additive_checksum;
+    uint32_t packet_count;
+    uint16_t most_recent_slot_count;
+    uint16_t min_slot_count;
+    uint16_t max_slot_count;
+    uint32_t packet_error_count;
+  });
+  STATIC_ASSERT(sizeof(comms_status_nsc_s) == 19);
+
+  struct comms_status_nsc_s comms_status_nsc;
+  comms_status_nsc.supported_fields = status->SupportedFieldsBitMask();
+  comms_status_nsc.additive_checksum = HostToNetwork(
+      status->AdditiveChecksum());
+  comms_status_nsc.packet_count = HostToNetwork(status->PacketCount());
+  comms_status_nsc.most_recent_slot_count = HostToNetwork(
+      status->MostRecentSlotCount());
+  comms_status_nsc.min_slot_count = HostToNetwork(status->MinSlotCount());
+  comms_status_nsc.max_slot_count = HostToNetwork(status->MaxSlotCount());
+  comms_status_nsc.packet_error_count = HostToNetwork(
+      status->PacketErrorCount());
+  return GetResponseFromData(
+    request,
+    reinterpret_cast<const uint8_t*>(&comms_status_nsc),
+    sizeof(comms_status_nsc),
+    RDM_ACK,
+    queued_message_count);
+}
+
+/**
+ * Set NSC comms status
+ */
+RDMResponse *ResponderHelper::SetCommsStatusNSC(
+    const RDMRequest *request,
+    NSCStatus *status,
+    uint8_t queued_message_count) {
+  if (request->ParamDataSize()) {
+    return NackWithReason(request, NR_FORMAT_ERROR, queued_message_count);
+  }
+
+  // Reset the counts...
+  status->Reset();
+
+  return GetResponseFromData(request, NULL, queued_message_count);
+}
+
 RDMResponse *ResponderHelper::GetListTags(
     const RDMRequest *request,
     const TagSet *tag_set,
@@ -1187,6 +1246,63 @@ RDMResponse *ResponderHelper::SetClearTags(
   return ResponderHelper::EmptySetResponse(request, queued_message_count);
 }
 
+RDMResponse *ResponderHelper::GetMetadataParameterVersion(
+    const RDMRequest *request,
+    uint16_t pid,
+    uint16_t version,
+    uint8_t queued_message_count) {
+  PACK(
+  struct metadata_parameter_version_s {
+    uint16_t pid;
+    uint16_t version;
+  });
+  STATIC_ASSERT(sizeof(metadata_parameter_version_s) == 4);
+
+  struct metadata_parameter_version_s metadata_param_version;
+  metadata_param_version.pid = HostToNetwork(pid);
+  metadata_param_version.version = HostToNetwork(version);
+
+  return GetResponseFromData(
+      request,
+      reinterpret_cast<uint8_t*>(&metadata_param_version),
+      sizeof(metadata_parameter_version_s),
+      RDM_ACK,
+      queued_message_count);
+}
+
+RDMResponse *ResponderHelper::GetMetadataJSON(
+    const RDMRequest *request,
+    uint16_t pid,
+    const string json,
+    uint8_t queued_message_count) {
+  PACK(
+  struct metadata_json_s {
+    uint16_t pid;
+    // TODO(Peter): This should effectively be unlimited...?
+    char json[(UINT8_MAX - 2)];
+  });
+  STATIC_ASSERT(sizeof(metadata_json_s) == UINT8_MAX);
+
+  struct metadata_json_s metadata_json;
+  metadata_json.pid = HostToNetwork(pid);
+
+  size_t str_len = min(json.size(),
+                       sizeof(metadata_json.json));
+  strncpy(metadata_json.json, json.c_str(), str_len);
+
+  unsigned int param_data_size = (
+      sizeof(metadata_json) -
+      sizeof(metadata_json.json) + str_len);
+
+  return GetResponseFromData(
+      request,
+      reinterpret_cast<uint8_t*>(&metadata_json),
+      param_data_size,
+      RDM_ACK,
+      queued_message_count);
+}
+
+
 /**
  * @brief Handle a request that returns a string
  * @note this truncates the string to max_length
diff --git a/common/utils/DmxBuffer.cpp b/common/utils/DmxBuffer.cpp
index b0a3b65fb..213c6763c 100644
--- a/common/utils/DmxBuffer.cpp
+++ b/common/utils/DmxBuffer.cpp
@@ -110,6 +110,15 @@ bool DmxBuffer::operator!=(const DmxBuffer &other) const {
 }
 
 
+unsigned int DmxBuffer::AdditiveChecksum() const {
+  unsigned int checksum = 0;
+  for (unsigned int i = 0; i < Size(); i++) {
+    checksum += m_data[i];
+  }
+  return checksum;
+}
+
+
 bool DmxBuffer::HTPMerge(const DmxBuffer &other) {
   if (!m_data) {
     if (!Init())
diff --git a/common/utils/DmxBufferTest.cpp b/common/utils/DmxBufferTest.cpp
index 74e6fd2e2..0b9847133 100644
--- a/common/utils/DmxBufferTest.cpp
+++ b/common/utils/DmxBufferTest.cpp
@@ -37,6 +37,7 @@ class DmxBufferTest: public CppUnit::TestFixture {
   CPPUNIT_TEST(testStringGetSet);
   CPPUNIT_TEST(testAssign);
   CPPUNIT_TEST(testCopy);
+  CPPUNIT_TEST(testAdditiveChecksum);
   CPPUNIT_TEST(testMerge);
   CPPUNIT_TEST(testStringToDmx);
   CPPUNIT_TEST(testCopyOnWrite);
@@ -52,6 +53,7 @@ class DmxBufferTest: public CppUnit::TestFixture {
     void testAssign();
     void testStringGetSet();
     void testCopy();
+    void testAdditiveChecksum();
     void testMerge();
     void testStringToDmx();
     void testCopyOnWrite();
@@ -261,6 +263,21 @@ void DmxBufferTest::testCopy() {
 }
 
 
+/*
+ * Check that the additive checksum works
+ */
+void DmxBufferTest::testAdditiveChecksum() {
+  DmxBuffer buffer1(TEST_DATA, sizeof(TEST_DATA));
+  OLA_ASSERT_EQ(15u, buffer1.AdditiveChecksum());
+
+  DmxBuffer buffer2(TEST_DATA2, sizeof(TEST_DATA2));
+  OLA_ASSERT_EQ(45u, buffer2.AdditiveChecksum());
+
+  DmxBuffer buffer3(TEST_DATA3, sizeof(TEST_DATA3));
+  OLA_ASSERT_EQ(33u, buffer3.AdditiveChecksum());
+}
+
+
 /*
  * Check that HTP Merging works
  */
diff --git a/include/ola/DmxBuffer.h b/include/ola/DmxBuffer.h
index 3dfd73adf..50c94943c 100644
--- a/include/ola/DmxBuffer.h
+++ b/include/ola/DmxBuffer.h
@@ -110,6 +110,12 @@ class DmxBuffer {
      */
     unsigned int Size() const { return m_length; }
 
+    /**
+     * @brief Additive checksum of DmxBuffer
+     * @return the additive checksum of slots in the buffer.
+     */
+    unsigned int AdditiveChecksum() const;
+
     /**
      * @brief HTP Merge from another DmxBuffer.
      * @param other the DmxBuffer to HTP merge into this one
diff --git a/include/ola/rdm/DummyResponder.h b/include/ola/rdm/DummyResponder.h
index f08e70272..ce1750420 100644
--- a/include/ola/rdm/DummyResponder.h
+++ b/include/ola/rdm/DummyResponder.h
@@ -135,6 +135,9 @@ class DummyResponder: public RDMControllerInterface {
   RDMResponse *GetFirmwareURL(const RDMRequest *request);
   RDMResponse *GetTestData(const RDMRequest *request);
   RDMResponse *SetTestData(const RDMRequest *request);
+  RDMResponse *GetMetadataParameterVersion(const RDMRequest *request);
+  RDMResponse *GetMetadataJSON(const RDMRequest *request);
+  RDMResponse *GetMetadataJSONURL(const RDMRequest *request);
 
   static const ResponderOps<DummyResponder>::ParamHandler PARAM_HANDLERS[];
   static const uint8_t DEFAULT_PERSONALITY = 2;
diff --git a/include/ola/rdm/Makefile.mk b/include/ola/rdm/Makefile.mk
index d7ba60563..11c9a4704 100644
--- a/include/ola/rdm/Makefile.mk
+++ b/include/ola/rdm/Makefile.mk
@@ -34,6 +34,7 @@ olardminclude_HEADERS = \
     include/ola/rdm/RDMReply.h \
     include/ola/rdm/ResponderHelper.h \
     include/ola/rdm/ResponderLoadSensor.h \
+    include/ola/rdm/ResponderNSCStatus.h \
     include/ola/rdm/ResponderOps.h \
     include/ola/rdm/ResponderOpsPrivate.h \
     include/ola/rdm/ResponderPersonality.h \
diff --git a/include/ola/rdm/OpenLightingEnums.h b/include/ola/rdm/OpenLightingEnums.h
index 42156dd87..9260ce920 100644
--- a/include/ola/rdm/OpenLightingEnums.h
+++ b/include/ola/rdm/OpenLightingEnums.h
@@ -50,6 +50,13 @@ typedef enum {
   OLA_MANUFACTURER_PID_CODE_VERSION = 0x8001,
 } rdm_ola_manufacturer_pid;
 
+// TODO(Peter): Some sort of vector against rdm_ola_manufacturer_pid or
+// something nicer?
+typedef enum {
+  OLA_MANUFACTURER_PID_JSON_VERSION_SERIAL_NUMBER = 1,
+  OLA_MANUFACTURER_PID_JSON_VERSION_CODE_VERSION = 1,
+} rdm_ola_manufacturer_pid_json_version;
+
 /**
  * Also see the list here
  * https://wiki.openlighting.org/index.php/Open_Lighting_Allocations#RDM_Model_Numbers
@@ -77,6 +84,7 @@ typedef enum {
 
 extern const char OLA_MANUFACTURER_LABEL[];
 extern const char OLA_MANUFACTURER_URL[];
+extern const char OLA_MANUFACTURER_PID_JSON_CODE_VERSION[];
 }  // namespace rdm
 }  // namespace ola
 #endif  // INCLUDE_OLA_RDM_OPENLIGHTINGENUMS_H_
diff --git a/include/ola/rdm/RDMEnums.h b/include/ola/rdm/RDMEnums.h
index 8f8b3a10b..46b9f6e4c 100644
--- a/include/ola/rdm/RDMEnums.h
+++ b/include/ola/rdm/RDMEnums.h
@@ -761,6 +761,29 @@ static const uint8_t DNS_NAME_SERVER_MAX_INDEX = 2;
 // Consts for E1.37-5
 static const uint16_t MAX_RDM_TEST_DATA_PATTERN_LENGTH = 4096;
 
+// bit masks for NSC status
+static const uint8_t NSC_STATUS_ADDITIVE_CHECKSUM_SUPPORTED_VALUE = 0x01;
+static const uint8_t NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE = 0x02;
+static const uint8_t NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE = 0x04;
+static const uint8_t NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE = 0x08;
+static const uint8_t NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE = 0x10;
+static const uint8_t NSC_STATUS_PACKET_ERROR_COUNT_SUPPORTED_VALUE = 0x20;
+
+// Consts for NSC status when unsupported
+static const uint32_t NSC_STATUS_ADDITIVE_CHECKSUM_UNSUPPORTED = 0xFFFFFFFF;
+static const uint32_t NSC_STATUS_PACKET_COUNT_UNSUPPORTED = 0xFFFFFFFF;
+static const uint16_t NSC_STATUS_MOST_RECENT_SLOT_COUNT_UNSUPPORTED = 0xFFFF;
+static const uint16_t NSC_STATUS_MIN_SLOT_COUNT_UNSUPPORTED = 0xFFFF;
+static const uint16_t NSC_STATUS_MAX_SLOT_COUNT_UNSUPPORTED = 0xFFFF;
+static const uint32_t NSC_STATUS_PACKET_ERROR_COUNT_UNSUPPORTED = 0xFFFFFFFF;
+
+// Consts for NSC status max range
+static const uint32_t NSC_STATUS_PACKET_COUNT_MAX = 0xFFFFFFFE;
+static const uint16_t NSC_STATUS_MOST_RECENT_SLOT_COUNT_MAX = 0xFFFE;
+static const uint16_t NSC_STATUS_MIN_SLOT_COUNT_MAX = 0xFFFE;
+static const uint16_t NSC_STATUS_MAX_SLOT_COUNT_MAX = 0xFFFE;
+static const uint32_t NSC_STATUS_PACKET_ERROR_COUNT_MAX = 0xFFFFFFFE;
+
 // The shipping lock states
 typedef enum {
   SHIPPING_LOCK_STATE_UNLOCKED = 0x00,
diff --git a/include/ola/rdm/ResponderHelper.h b/include/ola/rdm/ResponderHelper.h
index 4256c4f62..1edcd22c1 100644
--- a/include/ola/rdm/ResponderHelper.h
+++ b/include/ola/rdm/ResponderHelper.h
@@ -32,6 +32,7 @@
 #include <ola/network/Interface.h>
 #include <ola/rdm/NetworkManagerInterface.h>
 #include <ola/rdm/RDMCommand.h>
+#include <ola/rdm/ResponderNSCStatus.h>
 #include <ola/rdm/ResponderPersonality.h>
 #include <ola/rdm/ResponderSensor.h>
 #include <ola/rdm/ResponderTagSet.h>
@@ -151,20 +152,20 @@ class ResponderHelper {
         uint32_t min_value,
         uint32_t default_value,
         uint32_t max_value,
-        std::string description,
+        const std::string description,
         uint8_t queued_message_count = 0);
     static RDMResponse *GetASCIIParamDescription(
         const RDMRequest *request,
         uint16_t pid,
         rdm_command_class command_class,
-        std::string description,
+        const std::string description,
         uint8_t queued_message_count = 0);
     static RDMResponse *GetBitFieldParamDescription(
         const RDMRequest *request,
         uint16_t pid,
         uint8_t pdl_size,
         rdm_command_class command_class,
-        std::string description,
+        const std::string description,
         uint8_t queued_message_count = 0);
 
     static RDMResponse *GetRealTimeClock(
@@ -226,6 +227,16 @@ class ResponderHelper {
         const RDMRequest *request,
         uint8_t queued_message_count = 0);
 
+    static RDMResponse *GetCommsStatusNSC(
+        const RDMRequest *request,
+        const NSCStatus *status,
+        uint8_t queued_message_count = 0);
+
+    static RDMResponse *SetCommsStatusNSC(
+        const RDMRequest *request,
+        NSCStatus *status,
+        uint8_t queued_message_count = 0);
+
     static RDMResponse *GetListTags(
         const RDMRequest *request,
         const TagSet *tag_set,
@@ -251,6 +262,18 @@ class ResponderHelper {
         TagSet *tag_set,
         uint8_t queued_message_count = 0);
 
+    static RDMResponse *GetMetadataParameterVersion(
+        const RDMRequest *request,
+        uint16_t pid,
+        uint16_t version,
+        uint8_t queued_message_count = 0);
+
+    static RDMResponse *GetMetadataJSON(
+        const RDMRequest *request,
+        uint16_t pid,
+        const std::string json,
+        uint8_t queued_message_count = 0);
+
     // Generic Helpers.
     static RDMResponse *GetString(
         const RDMRequest *request,
diff --git a/include/ola/rdm/ResponderNSCStatus.h b/include/ola/rdm/ResponderNSCStatus.h
new file mode 100644
index 000000000..c5740eff4
--- /dev/null
+++ b/include/ola/rdm/ResponderNSCStatus.h
@@ -0,0 +1,239 @@
+/*
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * ResponderNSCStatus.h
+ * Manages the NSC status for a RDM responder.
+ * Copyright (C) 2025 Peter Newman
+ */
+
+/**
+ * @addtogroup rdm_resp
+ * @{
+ * @file ResponderNSCStatus.h
+ * @brief Manages the information about NSC status.
+ * @}
+ */
+
+#ifndef INCLUDE_OLA_RDM_RESPONDERNSCSTATUS_H_
+#define INCLUDE_OLA_RDM_RESPONDERNSCSTATUS_H_
+
+#include <ola/rdm/RDMEnums.h>
+#include <ola/DmxBuffer.h>
+#include <stdint.h>
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+namespace ola {
+namespace rdm {
+
+/**
+ * @brief Holds information about NSC status.
+ */
+class NSCStatus {
+ public:
+  struct NSCStatusOptions {
+   public:
+    bool additive_checksum_support;
+    bool packet_count_support;
+    bool most_recent_slot_count_support;
+    bool min_slot_count_support;
+    bool max_slot_count_support;
+    bool packet_error_count_support;
+
+    // NSCStatusOptions constructor to set all options, for use in
+    // initialisation lists. This also sets the defaults if called with no
+    // args
+    NSCStatusOptions(bool _additive_checksum_support = true,
+                     bool _packet_count_support = true,
+                     bool _most_recent_slot_count_support = true,
+                     bool _min_slot_count_support = true,
+                     bool _max_slot_count_support = true,
+                     bool _packet_error_count_support = false)
+        : additive_checksum_support(_additive_checksum_support),
+          packet_count_support(_packet_count_support),
+          most_recent_slot_count_support(_most_recent_slot_count_support),
+          min_slot_count_support(_min_slot_count_support),
+          max_slot_count_support(_max_slot_count_support),
+          packet_error_count_support(_packet_error_count_support) {
+    }
+  };
+
+  explicit NSCStatus(const NSCStatusOptions &options)
+      : m_additive_checksum_support(options.additive_checksum_support),
+        m_packet_count_support(options.packet_count_support),
+        m_most_recent_slot_count_support(
+            options.most_recent_slot_count_support),
+        m_min_slot_count_support(options.min_slot_count_support),
+        m_max_slot_count_support(options.max_slot_count_support),
+        m_packet_error_count_support(options.packet_error_count_support),
+        m_additive_checksum(0),
+        m_packet_count(0),
+        m_most_recent_slot_count(0),
+        m_min_slot_count(0),
+        m_max_slot_count(0),
+        m_packet_error_count(0) {
+  }
+  virtual ~NSCStatus() {}
+
+  uint32_t AdditiveChecksum() const {
+    if (m_additive_checksum_support) {
+      return m_additive_checksum;
+    } else {
+      return NSC_STATUS_ADDITIVE_CHECKSUM_UNSUPPORTED;
+    }
+  }
+
+  uint32_t PacketCount() const {
+    if (m_packet_count_support) {
+      return m_packet_count;
+    } else {
+      return NSC_STATUS_PACKET_COUNT_UNSUPPORTED;
+    }
+  }
+
+  uint16_t MostRecentSlotCount() const {
+    if (m_most_recent_slot_count_support) {
+      return m_most_recent_slot_count;
+    } else {
+      return NSC_STATUS_MOST_RECENT_SLOT_COUNT_UNSUPPORTED;
+    }
+  }
+
+  uint16_t MinSlotCount() const {
+    if (m_min_slot_count_support) {
+      return m_min_slot_count;
+    } else {
+      return NSC_STATUS_MIN_SLOT_COUNT_UNSUPPORTED;
+    }
+  }
+
+  uint16_t MaxSlotCount() const {
+    if (m_max_slot_count_support) {
+      return m_max_slot_count;
+    } else {
+      return NSC_STATUS_MAX_SLOT_COUNT_UNSUPPORTED;
+    }
+  }
+
+  uint32_t PacketErrorCount() const {
+    if (m_packet_error_count_support) {
+      return m_packet_error_count;
+    } else {
+      return NSC_STATUS_PACKET_ERROR_COUNT_UNSUPPORTED;
+    }
+  }
+
+  /**
+   * @brief Update the statistics we can from the DmxBuffer
+   * @note A DmxBuffer can only contain 512 slots (plus the start code) so this
+   * will limit some edge cases.
+   * @sa ReportError()
+   */
+  void UpdateStats(const DmxBuffer &buffer) {
+    // Size() + 1 to account for the start code
+
+    m_additive_checksum = buffer.AdditiveChecksum();
+    m_most_recent_slot_count = std::min(NSC_STATUS_MOST_RECENT_SLOT_COUNT_MAX,
+                                        (uint16_t) (buffer.Size() + 1));
+
+    if (m_packet_count == 0) {
+      // We must set the buffer size explicitly here on the first packet or
+      // we'd be permanently stuck at 0
+      m_min_slot_count = std::min(NSC_STATUS_MIN_SLOT_COUNT_MAX,
+                                  (uint16_t) (buffer.Size() + 1));
+    } else {
+      m_min_slot_count = std::min(NSC_STATUS_MIN_SLOT_COUNT_MAX,
+                                  std::min(m_min_slot_count,
+                                           (uint16_t) (buffer.Size() + 1)));
+    }
+
+    m_max_slot_count = std::min(NSC_STATUS_MAX_SLOT_COUNT_MAX,
+                                std::max(m_max_slot_count,
+                                         (uint16_t) (buffer.Size() + 1)));
+
+    // Update packet counter last so we can use it to track whether this is the
+    // first packet or not
+    if (m_packet_count < NSC_STATUS_PACKET_COUNT_MAX) {
+      m_packet_count++;
+    }
+
+    // We can't establish error states from the buffer, so don't touch them
+    // here
+  }
+
+  /**
+   * @brief Report a NSC error to increment that counter
+   */
+  void ReportError() {
+    if (m_packet_error_count < NSC_STATUS_PACKET_ERROR_COUNT_MAX) {
+      m_packet_error_count++;
+    }
+  }
+
+  /**
+   * @brief Reset the NSC stats.
+   */
+  void Reset() {
+    m_additive_checksum = 0;
+    m_packet_count = 0;
+    m_most_recent_slot_count = 0;
+    m_min_slot_count = 0;
+    m_max_slot_count = 0;
+    m_packet_error_count = 0;
+  }
+
+  uint8_t SupportedFieldsBitMask() const {
+    uint8_t bit_mask = 0;
+
+    if (m_additive_checksum_support) {
+      bit_mask |= NSC_STATUS_ADDITIVE_CHECKSUM_SUPPORTED_VALUE;
+    }
+    if (m_packet_count_support) {
+      bit_mask |= NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE;
+    }
+    if (m_most_recent_slot_count_support) {
+      bit_mask |= NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE;
+    }
+    if (m_min_slot_count_support) {
+      bit_mask |= NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE;
+    }
+    if (m_max_slot_count_support) {
+      bit_mask |= NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE;
+    }
+    if (m_packet_error_count_support) {
+      bit_mask |= NSC_STATUS_PACKET_ERROR_COUNT_SUPPORTED_VALUE;
+    }
+    return bit_mask;
+  }
+
+ protected:
+  const bool m_additive_checksum_support;
+  const bool m_packet_count_support;
+  const bool m_most_recent_slot_count_support;
+  const bool m_min_slot_count_support;
+  const bool m_max_slot_count_support;
+  const bool m_packet_error_count_support;
+  uint32_t m_additive_checksum;
+  uint32_t m_packet_count;
+  uint16_t m_most_recent_slot_count;
+  uint16_t m_min_slot_count;
+  uint16_t m_max_slot_count;
+  uint32_t m_packet_error_count;
+};
+}  // namespace rdm
+}  // namespace ola
+#endif  // INCLUDE_OLA_RDM_RESPONDERNSCSTATUS_H_
diff --git a/plugins/dummy/DummyPortTest.cpp b/plugins/dummy/DummyPortTest.cpp
index 7593c46c7..3634a29be 100644
--- a/plugins/dummy/DummyPortTest.cpp
+++ b/plugins/dummy/DummyPortTest.cpp
@@ -242,6 +242,9 @@ void DummyPortTest::testSupportedParams() {
 
   uint16_t supported_params[] = {
     ola::rdm::PID_TEST_DATA,
+    ola::rdm::PID_METADATA_PARAMETER_VERSION,
+    ola::rdm::PID_METADATA_JSON,
+    ola::rdm::PID_METADATA_JSON_URL,
     ola::rdm::PID_PRODUCT_DETAIL_ID_LIST,
     ola::rdm::PID_DEVICE_MODEL_DESCRIPTION,
     ola::rdm::PID_MANUFACTURER_LABEL,
diff --git a/python/ola/RDMConstants.py b/python/ola/RDMConstants.py
index 736f75051..cb628bc84 100644
--- a/python/ola/RDMConstants.py
+++ b/python/ola/RDMConstants.py
@@ -20,8 +20,13 @@
 __author__ = 'nomis52@gmail.com (Simon Newton)'
 
 
+RDM_MAX_PARAM_DATA_LENGTH = 231
+
 RDM_ZERO_FOOTPRINT_DMX_ADDRESS = 0xFFFF
 
+RDM_ESTA_PID_MIN = 0x0000
+RDM_ESTA_PID_MAX = 0x7FDF
+
 RDM_MANUFACTURER_PID_MIN = 0x8000
 RDM_MANUFACTURER_PID_MAX = 0xFFDF
 
@@ -43,6 +48,20 @@
 
 RDM_MAX_TEST_DATA_PATTERN_LENGTH = 4096
 
+RDM_NSC_STATUS_ADDITIVE_CHECKSUM_SUPPORTED_VALUE = 0x01
+RDM_NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE = 0x02
+RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE = 0x04
+RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE = 0x08
+RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE = 0x10
+RDM_NSC_STATUS_PACKET_ERROR_COUNT_SUPPORTED_VALUE = 0x20
+
+RDM_NSC_STATUS_ADDITIVE_CHECKSUM_UNSUPPORTED = 0xFFFFFFFF
+RDM_NSC_STATUS_PACKET_COUNT_UNSUPPORTED = 0xFFFFFFFF
+RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_UNSUPPORTED = 0xFFFF
+RDM_NSC_STATUS_MIN_SLOT_COUNT_UNSUPPORTED = 0xFFFF
+RDM_NSC_STATUS_MAX_SLOT_COUNT_UNSUPPORTED = 0xFFFF
+RDM_NSC_STATUS_PACKET_ERROR_COUNT_UNSUPPORTED = 0xFFFFFFFF
+
 
 def _ReverseDict(input):
   output = {}
diff --git a/tools/rdm/ModelCollector.py b/tools/rdm/ModelCollector.py
index f688bfe26..88fe6b4ca 100644
--- a/tools/rdm/ModelCollector.py
+++ b/tools/rdm/ModelCollector.py
@@ -59,7 +59,10 @@ class ModelCollector(object):
    SLOT_DESCRIPTION,
    MANUFACTURER_URL,
    PRODUCT_URL,
-   FIRMWARE_URL) = range(18)
+   FIRMWARE_URL,
+   METADATA_PARAMETER_VERSION,
+   METADATA_JSON,
+   METADATA_JSON_URL) = range(20)
 
   def __init__(self, wrapper, pid_store):
     self.wrapper = wrapper
@@ -98,6 +101,8 @@ def _ResetData(self):
     self.outstanding_pid = None
     self.work_state = None
     self.manufacturer_pids = []
+    self.metadata_parameter_version_pids = []
+    self.metadata_json_pids = []
     self.slots = set()
     self.personalities = []
     self.sensors = []
@@ -143,6 +148,15 @@ def _GetLanguage(self):
       return this_device.get('language', DEFAULT_LANGUAGE)
     return None
 
+  def _GetParameterMetadata(self, pid):
+    this_device = self._GetDevice()
+    if this_device:
+      this_version = self._GetVersion()
+      if this_version:
+        return this_version.setdefault('parameter_metadata', {}).setdefault(
+            pid, {})
+    return None
+
   def _CheckPidSupported(self, pid):
     this_version = self._GetVersion()
     if this_version:
@@ -208,6 +222,12 @@ def _HandleResponse(self, unpacked_data):
       self._HandleProductURL(unpacked_data)
     elif self.work_state == self.FIRMWARE_URL:
       self._HandleFirmwareURL(unpacked_data)
+    elif self.work_state == self.METADATA_PARAMETER_VERSION:
+      self._HandleMetadataParameterVersion(unpacked_data)
+    elif self.work_state == self.METADATA_JSON:
+      self._HandleMetadataJSON(unpacked_data)
+    elif self.work_state == self.METADATA_JSON_URL:
+      self._HandleMetadataJSONURL(unpacked_data)
 
   def _HandleDeviceInfo(self, data):
     """Called when we get a DEVICE_INFO response."""
@@ -278,6 +298,9 @@ def _HandleSupportedParams(self, data):
             param_info['param_id'] <=
             RDMConstants.RDM_MANUFACTURER_PID_MAX):
           self.manufacturer_pids.append(param_info['param_id'])
+      # Duplicate the list of manufacturer PIDs for other processing
+      self.metadata_parameter_version_pids.extend(self.manufacturer_pids)
+      self.metadata_json_pids.extend(self.manufacturer_pids)
     self._NextState()
 
   def _HandleSoftwareVersionLabel(self, data):
@@ -405,6 +428,27 @@ def _HandleFirmwareURL(self, data):
     this_device['firmware_url'] = data['url']
     self._NextState()
 
+  def _HandleMetadataParameterVersion(self, data):
+    """Called when we get a METADATA_PARAMETER_VERSION response."""
+    if data is not None:
+      this_param = self._GetParameterMetadata(data['pid'])
+      this_param['metadata_parameter_version'] = data['version']
+    self._FetchNextMetadataParameterVersion()
+
+  def _HandleMetadataJSON(self, data):
+    """Called when we get a METADATA_JSON response."""
+    if data is not None:
+      this_param = self._GetParameterMetadata(data['pid'])
+      # We store the raw string here in case it's not valid JSON
+      this_param['metadata_json'] = data['json'],
+    self._FetchNextMetadataJSON()
+
+  def _HandleMetadataJSONURL(self, data):
+    """Called when we get a METADATA_JSON_URL response."""
+    this_device = self._GetDevice()
+    this_device['metadata_json_url'] = data['url']
+    self._NextState()
+
   def _NextState(self):
     """Move to the next state of information fetching."""
     if self.work_state == self.EMPTYING_QUEUE:
@@ -529,6 +573,22 @@ def _NextState(self):
         logging.debug("Skipping pid %s as it's not supported on this device" %
                       pid)
         self._NextState()
+    elif self.work_state == self.FIRMWARE_URL:
+      self.work_state = self.METADATA_PARAMETER_VERSION
+      self._FetchNextMetadataParameterVersion()
+    elif self.work_state == self.METADATA_PARAMETER_VERSION:
+      self.work_state = self.METADATA_JSON
+      self._FetchNextMetadataJSON()
+    elif self.work_state == self.METADATA_JSON:
+      # fetch metadata JSON URL
+      self.work_state = self.METADATA_JSON_URL
+      pid = self.pid_store.GetName('METADATA_JSON_URL')
+      if self._CheckPidSupported(pid):
+        self._GetPid(pid)
+      else:
+        logging.debug("Skipping pid %s as it's not supported on this device" %
+                      pid)
+        self._NextState()
     else:
       # this one is done, onto the next UID
       self._FetchNextUID()
@@ -634,6 +694,42 @@ def _FetchNextSlotDescription(self):
       logging.debug('No more slots to fetch SLOT_DESCRIPTION for')
       self._NextState()
 
+  def _FetchNextMetadataParameterVersion(self):
+    """Fetch the info for the next metadata parameter version, or proceed to
+       the next state if there are none left.
+    """
+    if self.metadata_parameter_version_pids:
+      manufacturer_pid = self.metadata_parameter_version_pids.pop(0)
+      pid = self.pid_store.GetName('METADATA_PARAMETER_VERSION')
+      self.rdm_api.Get(self.universe,
+                       self.uid,
+                       PidStore.ROOT_DEVICE,
+                       pid,
+                       self._RDMRequestComplete,
+                       [manufacturer_pid])
+      logging.debug('Sent METADATA_PARAMETER_VERSION request')
+      self.outstanding_pid = pid
+    else:
+      self._NextState()
+
+  def _FetchNextMetadataJSON(self):
+    """Fetch the info for the next metadata JSON, or proceed to the next state
+       if there are none left.
+    """
+    if self.metadata_json_pids:
+      manufacturer_pid = self.metadata_json_pids.pop(0)
+      pid = self.pid_store.GetName('METADATA_JSON')
+      self.rdm_api.Get(self.universe,
+                       self.uid,
+                       PidStore.ROOT_DEVICE,
+                       pid,
+                       self._RDMRequestComplete,
+                       [manufacturer_pid])
+      logging.debug('Sent METADATA_JSON request')
+      self.outstanding_pid = pid
+    else:
+      self._NextState()
+
   def _FetchQueuedMessages(self):
     """Fetch messages until the queue is empty."""
     pid = self.pid_store.GetName('QUEUED_MESSAGE')
diff --git a/tools/rdm/TestDefinitions.py b/tools/rdm/TestDefinitions.py
index e97b0f58b..b58555a83 100644
--- a/tools/rdm/TestDefinitions.py
+++ b/tools/rdm/TestDefinitions.py
@@ -21,18 +21,32 @@
 
 from ola.OlaClient import OlaClient, RDMNack
 from ola.PidStore import ROOT_DEVICE
-from ola.RDMConstants import (INTERFACE_HARDWARE_TYPE_ETHERNET,
-                              RDM_INTERFACE_INDEX_MAX, RDM_INTERFACE_INDEX_MIN,
-                              RDM_MANUFACTURER_PID_MAX,
-                              RDM_MANUFACTURER_PID_MIN,
-                              RDM_MANUFACTURER_SD_MAX, RDM_MANUFACTURER_SD_MIN,
-                              RDM_MAX_DOMAIN_NAME_LENGTH,
-                              RDM_MAX_HOSTNAME_LENGTH,
-                              RDM_MAX_SERIAL_NUMBER_LENGTH,
-                              RDM_MAX_STRING_LENGTH,
-                              RDM_MAX_TEST_DATA_PATTERN_LENGTH,
-                              RDM_MIN_HOSTNAME_LENGTH,
-                              RDM_ZERO_FOOTPRINT_DMX_ADDRESS)
+from ola.RDMConstants import (
+    INTERFACE_HARDWARE_TYPE_ETHERNET,
+    RDM_ESTA_PID_MAX, RDM_ESTA_PID_MIN,
+    RDM_INTERFACE_INDEX_MAX, RDM_INTERFACE_INDEX_MIN,
+    RDM_MANUFACTURER_PID_MAX, RDM_MANUFACTURER_PID_MIN,
+    RDM_MANUFACTURER_SD_MAX, RDM_MANUFACTURER_SD_MIN,
+    RDM_MAX_DOMAIN_NAME_LENGTH,
+    RDM_MAX_HOSTNAME_LENGTH,
+    RDM_MAX_PARAM_DATA_LENGTH,
+    RDM_MAX_SERIAL_NUMBER_LENGTH,
+    RDM_MAX_STRING_LENGTH,
+    RDM_MAX_TEST_DATA_PATTERN_LENGTH,
+    RDM_MIN_HOSTNAME_LENGTH,
+    RDM_NSC_STATUS_ADDITIVE_CHECKSUM_SUPPORTED_VALUE,
+    RDM_NSC_STATUS_ADDITIVE_CHECKSUM_UNSUPPORTED,
+    RDM_NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE,
+    RDM_NSC_STATUS_PACKET_COUNT_UNSUPPORTED,
+    RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE,
+    RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_UNSUPPORTED,
+    RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE,
+    RDM_NSC_STATUS_MIN_SLOT_COUNT_UNSUPPORTED,
+    RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE,
+    RDM_NSC_STATUS_MAX_SLOT_COUNT_UNSUPPORTED,
+    RDM_NSC_STATUS_PACKET_ERROR_COUNT_SUPPORTED_VALUE,
+    RDM_NSC_STATUS_PACKET_ERROR_COUNT_UNSUPPORTED,
+    RDM_ZERO_FOOTPRINT_DMX_ADDRESS)
 from ola.StringUtils import StringEscape
 from ola.testing.rdm import TestMixins
 from ola.testing.rdm.ExpectedResults import (RDM_GET, RDM_SET, AckGetResult,
@@ -600,7 +614,6 @@ def VerifyResult(self, response, fields):
 class GetMaxPacketSize(DeviceInfoTest, ResponderTestFixture):
   """Check if the responder can handle a packet of the maximum size."""
   CATEGORY = TestCategory.ERROR_CONDITIONS
-  MAX_PDL = 231
   PROVIDES = ['supports_max_sized_pdl']
 
   def Test(self):
@@ -610,16 +623,16 @@ def Test(self):
       self.AckGetResult(),  # Some crazy devices continue to ack
       InvalidResponse(
           advisory='Responder returned an invalid response to a command with '
-                   'PDL of %d' % self.MAX_PDL
+                   'PDL of %d' % RDM_MAX_PARAM_DATA_LENGTH
       ),
       TimeoutResult(
           advisory='Responder timed out to a command with PDL of %d' %
-                   self.MAX_PDL),
+                   RDM_MAX_PARAM_DATA_LENGTH),
     ])
     # Incrementing list, so we can find out which bit we have where in memory
     # if it overflows
     data = b''
-    for i in range(0, self.MAX_PDL):
+    for i in range(0, RDM_MAX_PARAM_DATA_LENGTH):
       data += b'%c' % i
     self.SendRawGet(ROOT_DEVICE, self.pid, data)
 
@@ -641,13 +654,13 @@ def Test(self):
       return
 
     self._lower = 1
-    self._upper = GetMaxPacketSize.MAX_PDL
+    self._upper = RDM_MAX_PARAM_DATA_LENGTH
     self.SendPacket()
 
   def SendPacket(self):
     if self._lower + 1 == self._upper:
       self.AddWarning('Max PDL supported is < %d, was %d' %
-                      (GetMaxPacketSize.MAX_PDL, self._lower))
+                      (RDM_MAX_PARAM_DATA_LENGTH, self._lower))
       self.Stop()
       return
 
@@ -709,8 +722,8 @@ class GetSupportedParameters(ResponderTestFixture):
   """GET supported parameters."""
   CATEGORY = TestCategory.CORE
   PID = 'SUPPORTED_PARAMETERS'
-  PROVIDES = ['manufacturer_parameters', 'supported_parameters',
-              'acks_supported_parameters']
+  PROVIDES = ['esta_parameters', 'manufacturer_parameters',
+              'supported_parameters', 'acks_supported_parameters']
 
   # Declaring support for any of these is a warning:
   MANDATORY_PIDS = ['SUPPORTED_PARAMETERS',
@@ -769,6 +782,7 @@ def Test(self):
 
   def VerifyResult(self, response, fields):
     if not response.WasAcked():
+      self.SetProperty('esta_parameters', [])
       self.SetProperty('manufacturer_parameters', [])
       self.SetProperty('supported_parameters', [])
       self.SetProperty('acks_supported_parameters', False)
@@ -786,6 +800,7 @@ def VerifyResult(self, response, fields):
       banned_pids[pid.value] = pid
 
     supported_parameters = []
+    esta_parameters = []
     manufacturer_parameters = []
     count_by_pid = {}
 
@@ -803,9 +818,23 @@ def VerifyResult(self, response, fields):
         continue
 
       supported_parameters.append(param_id)
-      if (param_id >= RDM_MANUFACTURER_PID_MIN and
-          param_id <= RDM_MANUFACTURER_PID_MAX):
+      if (param_id >= RDM_ESTA_PID_MIN and
+          param_id <= RDM_ESTA_PID_MAX):
+        esta_parameters.append(param_id)
+
+        pid = self.LookupPidValue(param_id)
+        if pid is None:
+          self.AddAdvisory(
+            'PID 0x%04hx listed in supported parameters but not in the OLA PID '
+            'data. Either OLA is out of date or PID isn\'t a valid ESTA PID' %
+            param_id)
+      elif (param_id >= RDM_MANUFACTURER_PID_MIN and
+               param_id <= RDM_MANUFACTURER_PID_MAX):
         manufacturer_parameters.append(param_id)
+      else:
+        self.AddWarning('PID 0x%04hx listed in supported parameters but not '
+                        'within the valid ESTA or manufacturer PID ranges' %
+                        param_id)
 
     # Check for duplicate PIDs
     for pid, count in count_by_pid.items():
@@ -815,9 +844,11 @@ def VerifyResult(self, response, fields):
           self.AddAdvisory('%s listed %d times in supported parameters' %
                            (pid_obj, count))
         else:
-          self.AddAdvisory('PID 0x%hx listed %d times in supported parameters' %
-                           (pid, count))
+          self.AddAdvisory(
+            'PID 0x%04hx listed %d times in supported parameters' %
+            (pid, count))
 
+    self.SetProperty('esta_parameters', esta_parameters)
     self.SetProperty('manufacturer_parameters', manufacturer_parameters)
     self.SetProperty('supported_parameters', supported_parameters)
 
@@ -826,6 +857,11 @@ def VerifyResult(self, response, fields):
       unsupported_pids = []
       for pid_name in pid_names:
         pid = self.LookupPid(pid_name)
+
+        if pid is None:
+          self.SetBroken('Failed to lookup info for PID %s' % pid_name)
+          return
+
         if pid.value in supported_parameters:
           supported_pids.append(pid.name)
         else:
@@ -1204,7 +1240,7 @@ def VerifyResult(self, response, fields):
       return
 
     if self.current_param != fields['pid']:
-      self.SetFailed('Request for pid 0x%hx returned pid 0x%hx' %
+      self.SetFailed('Request for pid 0x%04hx returned pid 0x%04hx' %
                      (self.current_param, fields['pid']))
 
     if fields['type'] != 0:
@@ -3000,7 +3036,7 @@ def VerifyResult(self, response, fields):
                                    StringEscape(fields['name'])))
 
   def CheckCondition(self, sensor_number, fields, lhs, predicate_str, rhs):
-    """Check for a condition and add a warning if it isn't true."""
+    """Check for a condition and add an advisory if it isn't true."""
     predicate = self.PREDICATE_DICT[predicate_str]
     if predicate(fields[lhs], fields[rhs]):
       self.AddAdvisory(
@@ -8079,40 +8115,6 @@ class SetFirmwareURLWithData(TestMixins.UnsupportedSetWithDataMixin,
   PID = 'FIRMWARE_URL'
 
 
-class AllSubDevicesGetMetadataJSONURL(TestMixins.AllSubDevicesGetMixin,
-                                      OptionalParameterTestFixture):
-  """Send a get METADATA_JSON_URL to ALL_SUB_DEVICES."""
-  PID = 'METADATA_JSON_URL'
-
-
-class GetMetadataJSONURL(TestMixins.GetURLMixin,
-                         OptionalParameterTestFixture):
-  """GET the metadata JSON URL."""
-  CATEGORY = TestCategory.RDM_INFORMATION
-  PID = 'METADATA_JSON_URL'
-  EXPECTED_FIELDS = ['url']
-  # Extend the existing allowed schemas
-  ALLOWED_SCHEMAS = ['http', 'https', 'ftp']
-
-
-class GetMetadataJSONURLWithData(TestMixins.GetWithDataMixin,
-                                 OptionalParameterTestFixture):
-  """GET METADATA_JSON_URL with data."""
-  PID = 'METADATA_JSON_URL'
-
-
-class SetMetadataJSONURL(TestMixins.UnsupportedSetMixin,
-                         OptionalParameterTestFixture):
-  """Attempt to SET METADATA_JSON_URL."""
-  PID = 'METADATA_JSON_URL'
-
-
-class SetMetadataJSONURLWithData(TestMixins.UnsupportedSetWithDataMixin,
-                                 OptionalParameterTestFixture):
-  """Attempt to SET METADATA_JSON_URL with data."""
-  PID = 'METADATA_JSON_URL'
-
-
 class AllSubDevicesGetShippingLock(TestMixins.AllSubDevicesGetMixin,
                                    OptionalParameterTestFixture):
   """Send a get SHIPPING_LOCK to ALL_SUB_DEVICES."""
@@ -8223,8 +8225,7 @@ class GetTestDataPatternLengthMaxStringLength(TestMixins.GetTestDataMixin,
 class GetTestDataPatternLengthMaxPDL(TestMixins.GetTestDataMixin,
                                      OptionalParameterTestFixture):
   """GET TEST_DATA with a pattern length of the max PDL."""
-  # TODO(Peter): Make this a constant
-  PATTERN_LENGTH = 231
+  PATTERN_LENGTH = RDM_MAX_PARAM_DATA_LENGTH
 
 
 class GetTestDataPatternLengthMaxPatternLength(TestMixins.GetTestDataMixin,
@@ -8258,6 +8259,246 @@ def Test(self):
     self.SendRawGet(ROOT_DEVICE, self.pid, data)
 
 
+class SetTestDataLoopbackDataLengthZero(TestMixins.SetTestDataMixin,
+                                        OptionalParameterTestFixture):
+  """SET TEST_DATA with loopback data with a length of 0."""
+  LOOPBACK_DATA_LENGTH = 0
+
+
+class SetTestDataLoopbackDataLengthOne(TestMixins.SetTestDataMixin,
+                                       OptionalParameterTestFixture):
+  """SET TEST_DATA with loopback data with a length of 1."""
+  LOOPBACK_DATA_LENGTH = 1
+
+
+class SetTestDataLoopbackDataLengthMaxStringLength(
+        TestMixins.SetTestDataMixin,
+        OptionalParameterTestFixture):
+  """SET TEST_DATA with loopback data with the max string length."""
+  LOOPBACK_DATA_LENGTH = RDM_MAX_STRING_LENGTH
+
+
+class SetTestDataLoopbackDataLengthMaxPDL(TestMixins.SetTestDataMixin,
+                                          OptionalParameterTestFixture):
+  """SET TEST_DATA with loopback data with a length of the max PDL."""
+  LOOPBACK_DATA_LENGTH = RDM_MAX_PARAM_DATA_LENGTH
+
+
+class AllSubDevicesGetCommsStatusNSC(TestMixins.AllSubDevicesGetMixin,
+                                     OptionalParameterTestFixture):
+  """Send a get COMMS_STATUS_NSC to ALL_SUB_DEVICES."""
+  PID = 'COMMS_STATUS_NSC'
+
+
+class GetCommsStatusNSC(TestMixins.GetMixin, OptionalParameterTestFixture):
+  """GET COMMS_STATUS_NSC."""
+  CATEGORY = TestCategory.NETWORK_MANAGEMENT
+  PID = 'COMMS_STATUS_NSC'
+  EXPECTED_FIELDS = ['supported_fields',
+                     'additive_checksum_of_most_recent_nsc_packet',
+                     'nsc_packet_count',
+                     'nsc_most_recent_slot_count',
+                     'nsc_minimum_slot_count',
+                     'nsc_maximum_slot_count',
+                     'nsc_error_count']
+  PROVIDES = ['nsc_supported_fields']
+
+  PREDICATE_DICT = {
+      '==': operator.eq,
+      '<': operator.lt,
+      '>': operator.gt,
+  }
+
+  def VerifyResult(self, response, fields):
+    # Call super to set provides etc
+    super(GetCommsStatusNSC, self).VerifyResult(response, fields)
+    if not response.WasAcked():
+      return
+
+    if self.CheckFieldSupport(fields,
+                              RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE
+                              ) and (
+        self.CheckFieldSupport(fields,
+                               RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE)):
+      self.CheckCondition(fields,
+                          'nsc_minimum_slot_count',
+                          '>',
+                          'nsc_maximum_slot_count')
+
+    if self.CheckFieldSupport(
+        fields,
+        RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE) and (
+        self.CheckFieldSupport(fields,
+                               RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE)):
+      self.CheckCondition(fields,
+                          'nsc_most_recent_slot_count',
+                          '<',
+                          'nsc_minimum_slot_count')
+    if self.CheckFieldSupport(
+        fields,
+        RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE) and (
+        self.CheckFieldSupport(fields,
+                               RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE)):
+      self.CheckCondition(fields,
+                          'nsc_most_recent_slot_count',
+                          '>',
+                          'nsc_maximum_slot_count')
+
+    self.CheckFieldBlanking(fields,
+                            'additive_checksum_of_most_recent_nsc_packet',
+                            RDM_NSC_STATUS_ADDITIVE_CHECKSUM_SUPPORTED_VALUE,
+                            RDM_NSC_STATUS_ADDITIVE_CHECKSUM_UNSUPPORTED)
+    self.CheckFieldBlanking(fields,
+                            'nsc_packet_count',
+                            RDM_NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE,
+                            RDM_NSC_STATUS_PACKET_COUNT_UNSUPPORTED)
+    self.CheckFieldBlanking(
+        fields,
+        'nsc_most_recent_slot_count',
+        RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE,
+        RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_UNSUPPORTED)
+    self.CheckFieldBlanking(fields,
+                            'nsc_minimum_slot_count',
+                            RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE,
+                            RDM_NSC_STATUS_MIN_SLOT_COUNT_UNSUPPORTED)
+    self.CheckFieldBlanking(fields,
+                            'nsc_maximum_slot_count',
+                            RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE,
+                            RDM_NSC_STATUS_MAX_SLOT_COUNT_UNSUPPORTED)
+    self.CheckFieldBlanking(fields,
+                            'nsc_error_count',
+                            RDM_NSC_STATUS_PACKET_ERROR_COUNT_SUPPORTED_VALUE,
+                            RDM_NSC_STATUS_PACKET_ERROR_COUNT_UNSUPPORTED)
+
+    self.CheckPacketCount(
+        fields,
+        'nsc_most_recent_slot_count',
+        RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE)
+    self.CheckPacketCount(
+        fields,
+        'nsc_minimum_slot_count',
+        RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE)
+    self.CheckPacketCount(
+        fields,
+        'nsc_maximum_slot_count',
+        RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE)
+
+    if fields['supported_fields'] & 0xc0:
+      self.AddWarning('Bits 7-6 in the supported fields are set')
+
+  def CheckFieldSupport(self, fields, bit):
+    return fields['supported_fields'] & bit
+
+  def CheckFieldBlanking(self, fields, field, bit, value):
+    """Check supported fields behaviour."""
+    if self.CheckFieldSupport(fields, bit):
+      if fields[field] == value:
+        self.AddAdvisory(
+            'Field %s set as supported, but value is the unsupported value '
+            '0x%hx' %
+            (field, value))
+    else:
+      if fields[field] != value:
+        self.AddAdvisory(
+            'Field %s set as not supported, but value isn\'t the unsupported '
+            'value 0x%hx (got 0x%hx)' %
+            (field, value, fields[field]))
+
+  def CheckPacketCount(self, fields, field, bit):
+    """Check packet count versus slot count behaviour."""
+    if self.CheckFieldSupport(
+        fields,
+        RDM_NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE
+       ) and self.CheckFieldSupport(
+           fields,
+           bit):
+      if fields['nsc_packet_count'] == 0:
+        if fields[field] > 0:
+          self.AddAdvisory(
+              'Field %s > 0 (got %d) despite packet count being zero' %
+              (field, fields[field]))
+      else:
+        if fields[field] == 0:
+          self.AddAdvisory(
+              'Field %s is 0 despite packet count being non-zero (got %d)' %
+              (field, fields['nsc_packet_count']))
+
+  def CheckCondition(self, fields, lhs, predicate_str, rhs):
+    """Check for a condition and add an advisory if it isn't true."""
+    predicate = self.PREDICATE_DICT[predicate_str]
+    if predicate(fields[lhs], fields[rhs]):
+      self.AddAdvisory(
+          '%s (%d) %s %s (%d)' %
+          (lhs, fields[lhs], predicate_str, rhs, fields[rhs]))
+
+
+class GetCommsStatusNSCWithData(TestMixins.GetWithDataMixin,
+                                OptionalParameterTestFixture):
+  """GET COMMS_STATUS_NSC with data."""
+  PID = 'COMMS_STATUS_NSC'
+
+
+class SetCommsStatusNSC(OptionalParameterTestFixture):
+  """SET COMMS_STATUS_NSC to reset the counters."""
+  CATEGORY = TestCategory.NETWORK_MANAGEMENT
+  PID = 'COMMS_STATUS_NSC'
+  REQUIRES = ['nsc_supported_fields']
+
+  def Test(self):
+    self.AddIfSetSupported(self.AckSetResult(action=self.VerifySet))
+    self.SendSet(ROOT_DEVICE, self.pid)
+
+  def VerifySet(self):
+    expected_fields = {
+        'supported_fields': self.Property('nsc_supported_fields'),
+    }
+
+    self.AddExpectedField(expected_fields,
+                          'additive_checksum_of_most_recent_nsc_packet',
+                          RDM_NSC_STATUS_ADDITIVE_CHECKSUM_SUPPORTED_VALUE,
+                          RDM_NSC_STATUS_ADDITIVE_CHECKSUM_UNSUPPORTED)
+    self.AddExpectedField(expected_fields,
+                          'nsc_packet_count',
+                          RDM_NSC_STATUS_PACKET_COUNT_SUPPORTED_VALUE,
+                          RDM_NSC_STATUS_PACKET_COUNT_UNSUPPORTED)
+    self.AddExpectedField(expected_fields,
+                          'nsc_most_recent_slot_count',
+                          RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_SUPPORTED_VALUE,
+                          RDM_NSC_STATUS_MOST_RECENT_SLOT_COUNT_UNSUPPORTED)
+    self.AddExpectedField(expected_fields,
+                          'nsc_minimum_slot_count',
+                          RDM_NSC_STATUS_MIN_SLOT_COUNT_SUPPORTED_VALUE,
+                          RDM_NSC_STATUS_MIN_SLOT_COUNT_UNSUPPORTED)
+    self.AddExpectedField(expected_fields,
+                          'nsc_maximum_slot_count',
+                          RDM_NSC_STATUS_MAX_SLOT_COUNT_SUPPORTED_VALUE,
+                          RDM_NSC_STATUS_MAX_SLOT_COUNT_UNSUPPORTED)
+    self.AddExpectedField(expected_fields,
+                          'nsc_error_count',
+                          RDM_NSC_STATUS_PACKET_ERROR_COUNT_SUPPORTED_VALUE,
+                          RDM_NSC_STATUS_PACKET_ERROR_COUNT_UNSUPPORTED)
+
+    self.AddIfGetSupported(self.AckGetResult(field_values=expected_fields))
+    self.SendGet(ROOT_DEVICE, self.pid)
+
+  def AddExpectedField(self, fields, field, bit, value):
+    """Add expected field value depending on if it's supported."""
+    if not self.Property('nsc_supported_fields') & bit:
+      # If not supported, expect the blank value
+      fields[field] = value
+    else:
+      # TODO(Peter): Deal with the fact there may have been a NSC packet in
+      # between set and get (advisory with a descriptive message is probably
+      # better)
+      fields[field] = 0
+
+
+class SetCommsStatusNSCWithData(TestMixins.SetWithDataMixin,
+                                OptionalParameterTestFixture):
+  """Send a SET COMMS_STATUS_NSC command with unnecessary data."""
+  PID = 'COMMS_STATUS_NSC'
+
+
 class AllSubDevicesGetListTags(TestMixins.AllSubDevicesGetMixin,
                                OptionalParameterTestFixture):
   """Send a get LIST_TAGS to ALL_SUB_DEVICES."""
@@ -8336,7 +8577,7 @@ class AllSubDevicesGetCheckTag(TestMixins.AllSubDevicesGetMixin,
                                OptionalParameterTestFixture):
   """Send a get CHECK_TAG to ALL_SUB_DEVICES."""
   PID = 'CHECK_TAG'
-  DATA = [b'foo']
+  DATA = ['foo']
 
 
 class GetCheckTagWithNoData(TestMixins.GetWithNoDataMixin,
@@ -8441,6 +8682,222 @@ class SetDeviceUnitNumberWithExtraData(TestMixins.SetWithDataMixin,
   DATA = b'foobar'
 
 
+class AllSubDevicesGetMetadataParameterVersion(TestMixins.AllSubDevicesGetMixin,
+                                               OptionalParameterTestFixture):
+  """Send a get METADATA_PARAMETER_VERSION to ALL_SUB_DEVICES."""
+  PID = 'METADATA_PARAMETER_VERSION'
+  DATA = [0x8001]
+
+
+class GetMetadataParameterVersion(OptionalParameterTestFixture):
+  """Check that GET METADATA_PARAMETER_VERSION works for any manufacturer
+     params.
+  """
+  CATEGORY = TestCategory.RDM_INFORMATION
+  PID = 'METADATA_PARAMETER_VERSION'
+  REQUIRES = ['manufacturer_parameters']
+
+  def Test(self):
+    self.params = self.Property('manufacturer_parameters')[:]
+    if len(self.params) == 0:
+      self.SetNotRun('No manufacturer params found')
+      # This case is tested in GetMetadataParameterVersionForNonManufacturerPid
+      return
+    self._GetParam()
+
+  def _GetParam(self):
+    if len(self.params) == 0:
+      self.Stop()
+      return
+
+    self.AddExpectedResults(
+      self.AckGetResult(action=self._GetParam))
+    self.current_param = self.params.pop()
+    self.SendGet(ROOT_DEVICE, self.pid, [self.current_param])
+
+  def VerifyResult(self, response, fields):
+    if not response.WasAcked():
+      return
+
+    if self.current_param != fields['pid']:
+      self.SetFailed('Request for pid 0x%04hx returned pid 0x%04hx' %
+                     (self.current_param, fields['pid']))
+
+
+class GetMetadataParameterVersionForNonManufacturerPid(
+        OptionalParameterTestFixture):
+  """GET METADATA_PARAMETER_VERSION for a non-manufacturer pid."""
+  CATEGORY = TestCategory.ERROR_CONDITIONS
+  PID = 'METADATA_PARAMETER_VERSION'
+  REQUIRES = ['manufacturer_parameters']
+
+  def Test(self):
+    device_info_pid = self.LookupPid('DEVICE_INFO')
+    results = [
+      self.NackGetResult(RDMNack.NR_UNKNOWN_PID),
+      self.NackGetResult(
+          RDMNack.NR_DATA_OUT_OF_RANGE,
+          advisory='Metadata Parameter Version appears to be supported but no '
+                   'manufacturer PIDs were declared'),
+    ]
+    if self.Property('manufacturer_parameters'):
+      results = self.NackGetResult(RDMNack.NR_DATA_OUT_OF_RANGE)
+
+    self.AddExpectedResults(results)
+    self.SendGet(ROOT_DEVICE, self.pid, [device_info_pid.value])
+
+
+class GetMetadataParameterVersionWithNoData(TestMixins.GetWithNoDataMixin,
+                                            OptionalParameterTestFixture):
+  """GET METADATA_PARAMETER_VERSION with no argument given."""
+  PID = 'METADATA_PARAMETER_VERSION'
+
+
+class GetMetadataParameterVersionWithExtraData(TestMixins.GetWithDataMixin,
+                                               OptionalParameterTestFixture):
+  """GET METADATA_PARAMETER_VERSION with more than 2 bytes of data."""
+  PID = 'METADATA_PARAMETER_VERSION'
+  DATA = b'foo'  # TODO(peter): Ensure the first 2 bytes are sane/valid.
+
+
+class SetMetadataParameterVersion(TestMixins.UnsupportedSetMixin,
+                                  OptionalParameterTestFixture):
+  """Attempt to SET METADATA_PARAMETER_VERSION."""
+  PID = 'METADATA_PARAMETER_VERSION'
+
+
+class SetMetadataParameterVersionWithData(
+        TestMixins.UnsupportedSetWithDataMixin,
+        OptionalParameterTestFixture):
+  """Attempt to SET METADATA_PARAMETER_VERSION with data."""
+  PID = 'METADATA_PARAMETER_VERSION'
+
+
+class AllSubDevicesGetMetadataJSON(TestMixins.AllSubDevicesGetMixin,
+                                   OptionalParameterTestFixture):
+  """Send a get METADATA_JSON to ALL_SUB_DEVICES."""
+  PID = 'METADATA_JSON'
+  DATA = [0x8001]
+
+
+class GetMetadataJSON(TestMixins.GetJSONMixin,
+                       OptionalParameterTestFixture):
+  """Check that GET METADATA_JSON works for any manufacturer params."""
+  CATEGORY = TestCategory.RDM_INFORMATION
+  PID = 'METADATA_JSON'
+  REQUIRES = ['manufacturer_parameters']
+  # JSON first, as it's the field we want to do JSON validation on
+  EXPECTED_FIELDS = ['json', 'pid']
+
+  def Test(self):
+    self.params = self.Property('manufacturer_parameters')[:]
+    if len(self.params) == 0:
+      self.SetNotRun('No manufacturer params found')
+      # This case is tested in GetMetadataJSONForNonManufacturerPid
+      return
+    self._GetParam()
+
+  def _GetParam(self):
+    if len(self.params) == 0:
+      self.Stop()
+      return
+
+    self.AddExpectedResults(
+      self.AckGetResult(action=self._GetParam))
+    self.current_param = self.params.pop()
+    self.SendGet(ROOT_DEVICE, self.pid, [self.current_param])
+
+  def VerifyResult(self, response, fields):
+    super(TestMixins.GetJSONMixin, self).VerifyResult(response, fields)
+
+    if self.current_param != fields['pid']:
+      self.SetFailed('Request for pid 0x%04hx returned pid 0x%04hx' %
+                     (self.current_param, fields['pid']))
+
+    # TODO(Peter): Validate JSON PID field too
+
+
+class GetMetadataJSONForNonManufacturerPid(OptionalParameterTestFixture):
+  """GET METADATA_JSON for a non-manufacturer pid."""
+  CATEGORY = TestCategory.ERROR_CONDITIONS
+  PID = 'METADATA_JSON'
+  REQUIRES = ['manufacturer_parameters']
+
+  def Test(self):
+    device_info_pid = self.LookupPid('DEVICE_INFO')
+    results = [
+      self.NackGetResult(RDMNack.NR_UNKNOWN_PID),
+      self.NackGetResult(
+          RDMNack.NR_DATA_OUT_OF_RANGE,
+          advisory='Metadata JSON appears to be supported but no '
+                   'manufacturer PIDs were declared'),
+    ]
+    if self.Property('manufacturer_parameters'):
+      results = self.NackGetResult(RDMNack.NR_DATA_OUT_OF_RANGE)
+
+    self.AddExpectedResults(results)
+    self.SendGet(ROOT_DEVICE, self.pid, [device_info_pid.value])
+
+
+class GetMetadataJSONWithNoData(TestMixins.GetWithNoDataMixin,
+                                OptionalParameterTestFixture):
+  """GET METADATA_JSON with no argument given."""
+  PID = 'METADATA_JSON'
+
+
+class GetMetadataJSONWithExtraData(TestMixins.GetWithDataMixin,
+                                   OptionalParameterTestFixture):
+  """GET METADATA_JSON with more than 2 bytes of data."""
+  PID = 'METADATA_JSON'
+  DATA = b'foo'  # TODO(peter): Ensure the first 2 bytes are sane/valid.
+
+
+class SetMetadataJSON(TestMixins.UnsupportedSetMixin,
+                      OptionalParameterTestFixture):
+  """Attempt to SET METADATA_JSON."""
+  PID = 'METADATA_JSON'
+
+
+class SetMetadataJSONWithData(TestMixins.UnsupportedSetWithDataMixin,
+                              OptionalParameterTestFixture):
+  """Attempt to SET METADATA_JSON with data."""
+  PID = 'METADATA_JSON'
+
+
+class AllSubDevicesGetMetadataJSONURL(TestMixins.AllSubDevicesGetMixin,
+                                      OptionalParameterTestFixture):
+  """Send a get METADATA_JSON_URL to ALL_SUB_DEVICES."""
+  PID = 'METADATA_JSON_URL'
+
+
+class GetMetadataJSONURL(TestMixins.GetURLMixin,
+                         OptionalParameterTestFixture):
+  """GET the metadata JSON URL."""
+  CATEGORY = TestCategory.RDM_INFORMATION
+  PID = 'METADATA_JSON_URL'
+  EXPECTED_FIELDS = ['url']
+  # Extend the existing allowed schemas
+  ALLOWED_SCHEMAS = ['http', 'https', 'ftp']
+
+
+class GetMetadataJSONURLWithData(TestMixins.GetWithDataMixin,
+                                 OptionalParameterTestFixture):
+  """GET METADATA_JSON_URL with data."""
+  PID = 'METADATA_JSON_URL'
+
+
+class SetMetadataJSONURL(TestMixins.UnsupportedSetMixin,
+                         OptionalParameterTestFixture):
+  """Attempt to SET METADATA_JSON_URL."""
+  PID = 'METADATA_JSON_URL'
+
+
+class SetMetadataJSONURLWithData(TestMixins.UnsupportedSetWithDataMixin,
+                                 OptionalParameterTestFixture):
+  """Attempt to SET METADATA_JSON_URL with data."""
+  PID = 'METADATA_JSON_URL'
+
+
 # E1.33/E1.37-7 PIDS
 # =============================================================================
 
diff --git a/tools/rdm/TestMixins.py b/tools/rdm/TestMixins.py
index b5e08e9dd..15bb20626 100644
--- a/tools/rdm/TestMixins.py
+++ b/tools/rdm/TestMixins.py
@@ -15,6 +15,7 @@
 # TestMixins.py
 # Copyright (C) 2010 Simon Newton
 
+import json
 import struct
 import sys
 
@@ -177,6 +178,12 @@ class GetURLMixin(GetMixin):
   # TODO(Peter): Make this a constant
   MAX_LENGTH = 231
   ALLOWED_SCHEMAS = ['http', 'https']
+  # TODO(Peter): Add non-English ones from https://en.wikipedia.org/wiki/.test
+  # From RFC 2606 and RFC 6762
+  DENIED_TLDS = ['test', 'example', 'internal', 'invalid', 'local',
+                 'localhost']
+  # From RFC 2606
+  DENIED_DOMAINS = ['example.com', 'example.net', 'example.org']
 
   def VerifyResult(self, response, fields):
     if not response.WasAcked():
@@ -225,12 +232,38 @@ def VerifyResult(self, response, fields):
         # TODO(Peter): Possibly check for ValueError locally here too...
         if url_result.netloc is None or not url_result.netloc:
           self.AddAdvisory(
-              '%s field in %s had no netloc' %
-              (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name))
-        # else:
-          # TODO(Peter): Check for a dot in the netloc
-          # TODO(Peter): Check the netloc domain isn't a prohibited internal
-          # only one
+              '%s field in %s had no netloc, was %s' %
+              (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name, url_field))
+        else:
+          if url_result.hostname is None or not url_result.hostname:
+            self.AddAdvisory(
+                '%s field in %s had no hostname, was %s' %
+                (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name,
+                 url_field))
+          else:
+            if '.' not in url_result.hostname:
+              self.AddAdvisory(
+                '%s field in %s had hostname without a dot, expecting an '
+                'FQDN, was %s' %
+                (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name,
+                 url_result.hostname))
+
+            for tld in self.DENIED_TLDS:
+              tld_with_dot = "." + tld
+              if url_result.hostname.endswith(tld_with_dot):
+                self.AddAdvisory(
+                  '%s field in %s had hostname ending with denied TLD %s' %
+                  (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name,
+                   tld_with_dot))
+
+            for domain in self.DENIED_DOMAINS:
+              domain_with_dot = "." + domain
+              if (url_result.hostname is domain or
+                  url_result.hostname.endswith(domain_with_dot)):
+                self.AddAdvisory(
+                    '%s field in %s had hostname ending with denied domain %s' %
+                    (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name,
+                     domain))
 
         # TODO(Peter): Optionally expect at least one other section
         # (product/firmware)
@@ -241,6 +274,57 @@ def VerifyResult(self, response, fields):
            url_field))
 
 
+class GetJSONMixin(GetMixin):
+  """GET Mixin for an optional JSON PID. Verify EXPECTED_FIELDS are in the
+    response.
+
+    This mixin also sets a property if PROVIDES is defined.  The target class
+    needs to defined EXPECTED_FIELDS and optionally PROVIDES.
+  """
+  # Min length is based on simplest empty JSON of {}
+  MIN_LENGTH = 2
+  # TODO(Peter): Max length is unlimited?
+  MAX_LENGTH = 255
+
+  def VerifyResult(self, response, fields):
+    if not response.WasAcked():
+      return
+
+    json_field = fields[self.EXPECTED_FIELDS[0]]
+
+    if self.PROVIDES:
+      self.SetProperty(self.PROVIDES[0], json_field)
+
+    if ContainsUnprintable(json_field):
+      self.AddAdvisory(
+          '%s field in %s contains unprintable characters, was %s' %
+          (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name,
+           StringEscape(json_field)))
+
+    if self.MIN_LENGTH and len(json_field) < self.MIN_LENGTH:
+      self.SetFailed(
+          '%s field in %s was shorter than expected, was %d, expected %d' %
+          (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name,
+           len(json_field), self.MIN_LENGTH))
+
+    if self.MAX_LENGTH and len(json_field) > self.MAX_LENGTH:
+      self.SetFailed(
+          '%s field in %s was longer than expected, was %d, expected %d' %
+          (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name,
+           len(json_field), self.MAX_LENGTH))
+
+    # TODO(Peter): Do basic JSON validation
+    try:
+      parse_json = json.loads(json_field)
+
+      # TODO(Peter): Add the option to do test-specific validation
+    except ValueError as err:
+      self.SetFailed(
+          '%s field in %s didn\'t parse as valid JSON due to %s, was %s' %
+          (self.EXPECTED_FIELDS[0].capitalize(), self.pid.name, str(err),
+           json_field))
+
+
 class GetTestDataMixin(ResponderTestFixture):
   """GET TEST_DATA PID with a given pattern length.
 
@@ -268,6 +352,38 @@ def Test(self):
     self.SendGet(PidStore.ROOT_DEVICE, self.pid, [self.PATTERN_LENGTH])
 
 
+class SetTestDataMixin(ResponderTestFixture):
+  """SET TEST_DATA PID with a given pattern length.
+
+    If ALLOWED_NACKS is non-empty, this adds a custom NackGetResult to the list
+    of allowed results for each entry.
+  """
+  PID = 'TEST_DATA'
+  CATEGORY = TestCategory.NETWORK_MANAGEMENT
+  LOOPBACK_DATA_LENGTH = 1
+  ALLOWED_NACKS = []
+  EXPECTED_FIELDS = ['loopback_data']
+
+  def Test(self):
+    expected_value = []
+    for i in reversed(range(0, self.LOOPBACK_DATA_LENGTH)):
+      expected_value.append({'data': (i % (255 + 1))})
+    results = [
+      self.AckSetResult(
+          field_names=self.EXPECTED_FIELDS,
+          field_values={self.EXPECTED_FIELDS[0]: expected_value})
+    ]
+    for nack in self.ALLOWED_NACKS:
+      results.append(self.NackSetResult(nack))
+    self.AddIfSetSupported(results)
+    data = b''
+    # Descending data to differentiate from GET TEST_DATA
+    for i in reversed(range(0, self.LOOPBACK_DATA_LENGTH)):
+      data += b'%c' % i
+    # TODO(Peter): using SendRawSet until we fix packing of groups in Python
+    self.SendRawSet(PidStore.ROOT_DEVICE, self.pid, data)
+
+
 class GetRequiredMixin(ResponderTestFixture):
   """GET Mixin for a required PID. Verify EXPECTED_FIELDS is in the response.