diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5e5ce4ab55ce84e8bef1219142651bffada6c26d..48f2c0c6012a432f1d0def948593732344adb579 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -50,8 +50,9 @@ test-das6:
     - module load cuda/12.2.1
     - module list
   script:
-    - cmake -S . -B build ${CMAKE_OPTIONS} -DPMT_BUILD_NVML=ON -DPMT_BUILD_BINARY=ON
+    - cmake -S . -B build ${CMAKE_OPTIONS} -DPMT_BUILD_NVML=ON -DPMT_BUILD_BINARY=ON -DPMT_BUILD_TESTING=ON
     - make -C build
+    - CTEST_OUTPUT_ON_FAILURE=1 make -C build test
     - $(pwd)/build/bin/PMT --name rapl -- sleep 3
     - $(pwd)/build/bin/PMT --name nvml -- sleep 3
     - PYTHONPATH=$PYTHONPATH:$(pwd)/build/python/site-packages python3 ${CI_PROJECT_DIR}/python/demo.py
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 426b10fc9356c24cd3dedfe31954c5c66271ae10..6da8654f1ba001a1ced2b739b04a04d308575422 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -26,6 +26,14 @@ find_package(Threads REQUIRED)
 # Enable compile commands (needed for iwyu)
 set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
 
+# Option to enable testing
+option(PMT_BUILD_TESTING "" OFF)
+
+if(${PMT_BUILD_TESTING})
+  enable_testing()
+  add_subdirectory(test)
+endif()
+
 # Options to disable implementations
 # that require external dependencies
 option(PMT_BUILD_CRAY "" OFF)
@@ -124,11 +132,19 @@ set_target_properties(
 # Replicate header file structure in binary directory
 file(MAKE_DIRECTORY "${PROJECT_BINARY_DIR}/pmt")
 file(MAKE_DIRECTORY "${PROJECT_BINARY_DIR}/pmt/common")
-file(CREATE_LINK "${CMAKE_CURRENT_SOURCE_DIR}/common/PMT.h"
-     "${PROJECT_BINARY_DIR}/pmt/common/PMT.h" SYMBOLIC)
+list(APPEND PMT_HEADER_FILES "common/PMT.h")
+list(APPEND PMT_HEADER_FILES "common/State.h")
+list(APPEND PMT_HEADER_FILES "common/Timestamp.h")
 foreach(PMT_HEADER_FILE ${PMT_HEADER_FILES})
   set(LINK_SRC "${CMAKE_CURRENT_SOURCE_DIR}/${PMT_HEADER_FILE}")
-  get_filename_component(LINK_DST ${PMT_HEADER_FILE} NAME)
+  # Strip the subdirectory from the header file path,
+  # except for header files from the 'common' subdirectory.
+  string(FIND "${PMT_HEADER_FILE}" "common" PREFIX_POSITION)
+  if(PREFIX_POSITION EQUAL 0)
+    set(LINK_DST ${PMT_HEADER_FILE})
+  else()
+    get_filename_component(LINK_DST ${PMT_HEADER_FILE} NAME)
+  endif()
   file(CREATE_LINK ${LINK_SRC} "${PROJECT_BINARY_DIR}/pmt/${LINK_DST}" SYMBOLIC)
 endforeach()
 
diff --git a/amdsmi/AMDSMIImpl.cpp b/amdsmi/AMDSMIImpl.cpp
index 5910fed3a8f2c4df1b4777ca78ccaf1e2f558bb1..231561b0fd1b2122b6869b3fc3393d5ea7086e1c 100644
--- a/amdsmi/AMDSMIImpl.cpp
+++ b/amdsmi/AMDSMIImpl.cpp
@@ -2,18 +2,19 @@
 #include <vector>
 
 #include "AMDSMIImpl.h"
+#include "common/Exception.h"
 
 namespace {
-class AmdsmiError : public std::exception {
+class AmdsmiError : public pmt::Exception {
  public:
   explicit AmdsmiError(amdsmi_status_t result, const char *file, int line)
       : result_(result), file_(file), line_(line) {}
 
   const char *what() const noexcept override {
-    std::ostringstream oss;
-    oss << "AMDSMI call failed with status: " << result_ << " in " << file_
-        << " at line " << line_;
-    message_ = oss.str();
+    std::ostringstream message;
+    message << "AMDSMI call failed with status: " << result_ << " in " << file_
+            << " at line " << line_;
+    message_ = message.str();
     return message_.c_str();
   }
 
@@ -78,34 +79,16 @@ namespace pmt::amdsmi {
 
 AMDSMIImpl::AMDSMIImpl(const unsigned device_number) {
   processor_ = Initialize(device_number);
-
-  state_previous_ = GetAMDSMIState();
-  state_previous_.joules_ = 0;
 }
 
 AMDSMIImpl::~AMDSMIImpl() { checkAmdsmiCall(amdsmi_shut_down()); }
 
-AMDSMIState::operator State() {
+State AMDSMIImpl::GetState() {
   State state;
-  state.timestamp_ = timestamp_;
-  state.name_[0] = "device";
-  state.joules_[0] = joules_;
-  state.watt_[0] = watt_;
-  return state;
-}
-
-AMDSMIState AMDSMIImpl::GetAMDSMIState() {
-  AMDSMIState state;
   state.timestamp_ = GetTime();
-  state.watt_ = GetPower(processor_);
-  state.joules_ = state_previous_.joules_;
-  const float watt = (state.watt_ + state_previous_.watt_) / 2;
-  const double duration = seconds(state_previous_.timestamp_, state.timestamp_);
-  state.joules_ += watt * duration;
-  state_previous_ = state;
+  state.name_[0] = "device";
+  state.watt_[0] = GetPower(processor_);
   return state;
 }
 
-State AMDSMIImpl::GetState() { return GetAMDSMIState(); }
-
 }  // end namespace pmt::amdsmi
\ No newline at end of file
diff --git a/amdsmi/AMDSMIImpl.h b/amdsmi/AMDSMIImpl.h
index a8622e52421335703fdba495a54d15350979db71..3a9dbebd285fbd0a9730d1e37e9e82f22b5fa6a2 100644
--- a/amdsmi/AMDSMIImpl.h
+++ b/amdsmi/AMDSMIImpl.h
@@ -4,14 +4,6 @@
 
 namespace pmt::amdsmi {
 
-class AMDSMIState {
- public:
-  operator State();
-  Timestamp timestamp_;
-  double watt_ = 0;
-  double joules_ = 0;
-};
-
 class AMDSMIImpl : public AMDSMI {
  public:
   AMDSMIImpl(const unsigned device_number);
@@ -25,9 +17,6 @@ class AMDSMIImpl : public AMDSMI {
   }
 
   amdsmi_processor_handle processor_;
-
-  AMDSMIState state_previous_;
-  AMDSMIState GetAMDSMIState();
 };
 
 }  // end namespace pmt::amdsmi
\ No newline at end of file
diff --git a/bin/main.cpp b/bin/main.cpp
index 87cd22cd6336a868c64716327788271704773f0b..b139881caec527067bd614d451136c8d3fd6c3a8 100644
--- a/bin/main.cpp
+++ b/bin/main.cpp
@@ -64,7 +64,7 @@ void run(pmt::PMT& sensor, const std::vector<std::string>& command) {
       std::cout << std::endl;
     }
   } else {
-    std::stringstream command_stream;
+    std::ostringstream command_stream;
     for (int i = 1; i < command.size(); i++) {
       if (i > 1) {
         command_stream << " ";
diff --git a/cmake/modules/AddSensor.cmake b/cmake/modules/AddSensor.cmake
index d86fb0054e4b737083c6116d937e25bcb2070428..d450ad73e6dfca5f3c32b489b01b005e9c5dbaa6 100644
--- a/cmake/modules/AddSensor.cmake
+++ b/cmake/modules/AddSensor.cmake
@@ -26,6 +26,8 @@ macro(add_sensor)
     target_include_directories(${LIBRARY_NAME}
                                PRIVATE ${ADD_SENSOR_INCLUDE_DIRECTORIES})
   endif()
+  get_filename_component(PMT_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY)
+  target_include_directories(${LIBRARY_NAME} PRIVATE ${PMT_INCLUDE_DIR})
 
   # Link to the global scope
   install(FILES ${ADD_SENSOR_HEADER} DESTINATION include/pmt)
diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
index 0041079a7934a5ecefb0583a0affa1909695e9ea..1a1f44989c0b8099010d2f2f59c5f991979cd910 100644
--- a/common/CMakeLists.txt
+++ b/common/CMakeLists.txt
@@ -1,6 +1,6 @@
 project(pmt-common)
 
-add_library(${PROJECT_NAME} OBJECT PMT.cpp)
+add_library(${PROJECT_NAME} OBJECT PMT.cpp State.cpp)
 target_include_directories(${PROJECT_NAME} PRIVATE ${PMT_INCLUDE_DIR})
 
 install(FILES PMT.h DESTINATION include/pmt/common)
diff --git a/common/Exception.h b/common/Exception.h
new file mode 100644
index 0000000000000000000000000000000000000000..3bfcba8e1bff0672c25dd109fd5c9f6acc9e8495
--- /dev/null
+++ b/common/Exception.h
@@ -0,0 +1,18 @@
+#ifndef PMT_EXCEPTION_H_
+#define PMT_EXCEPTION_H_
+
+#include <exception>
+
+namespace pmt {
+class Exception : public std::exception {
+ public:
+  explicit Exception(const char *message = "") : message_(message) {}
+
+  const char *what() const noexcept override { return message_; }
+
+ private:
+  const char *message_;
+};
+}  // namespace pmt
+
+#endif  // PMT_EXCEPTION_H_
\ No newline at end of file
diff --git a/common/PMT.cpp b/common/PMT.cpp
index a209e25e70516076a2e5bc48695ad44d927761d2..1448aa58845a230b6e57a3887290d38830d8de6a 100644
--- a/common/PMT.cpp
+++ b/common/PMT.cpp
@@ -5,10 +5,13 @@
 #include <cstdlib>
 #include <cstring>
 #include <iomanip>
+#include <iostream>
 
 #include <pmt.h>
 
+#include "common/Exception.h"
 #include "common/PMT.h"
+
 namespace {
 template <typename T>
 bool isEqual(T x, T y) {
@@ -36,9 +39,9 @@ double PMT::seconds(const Timestamp &timestamp) {
 }
 
 double PMT::seconds(const Timestamp &first, const Timestamp &second) {
-  return std::chrono::duration_cast<std::chrono::microseconds>((second - first))
+  return std::chrono::duration_cast<std::chrono::microseconds>(second - first)
              .count() /
-         1.e6;
+         1e6;
 }
 
 double PMT::seconds(const State &first, const State &second) {
@@ -64,42 +67,61 @@ unsigned int PMT::GetDumpInterval() {
   }
 }
 
-std::string State::name(int i) {
-  assert(i < nr_measurements_);
-  return name_[i];
-}
-
-float State::joules(int i) {
-  assert(i < nr_measurements_);
-  return joules_[i];
-}
-
-float State::watts(int i) {
-  assert(i < nr_measurements_);
-  return watt_[i];
-}
-
 void PMT::StartThread() {
-  SetMeasurementInterval();
+  thread_mutex_.lock();
 
   thread_ = std::thread([&] {
-    State state_previous = GetState();
-    assert(state_previous.nr_measurements_ > 0);
-    state_latest_ = state_previous;
+    try {
+      state_latest_ = GetState();
+    } catch (const pmt::Exception &e) {
+#if defined(DEBUG)
+      std::cerr << "GetState(): " << e.what() << std::endl;
+#endif
+      throw pmt::Exception("Could not read initial state.");
+    }
 
     if (dump_file_) {
-      DumpHeader(state_previous);
+      DumpHeader(state_latest_);
     }
 
     while (!thread_stop_) {
       std::this_thread::sleep_for(
-          std::chrono::milliseconds(GetMeasurementInterval()));
-      state_latest_ = GetState();
+          std::chrono::milliseconds(measurement_interval_));
+
+      State state;
+      try {
+        state = GetState();
+      } catch (const pmt::Exception &e) {
+#if defined(DEBUG)
+        std::cerr << "GetState(): " << e.what() << std::endl;
+#endif
+        continue;
+      }
 
       if (dump_file_ &&
-          (1e3 * seconds(state_previous, state_latest_)) > GetDumpInterval()) {
-        Dump(state_latest_);
-        state_previous = state_latest_;
+          (1e3 * seconds(state_latest_, state)) > GetDumpInterval()) {
+        Dump(state);
+      }
+
+      for (int i = 0; i < state.NrMeasurements(); i++) {
+        const double seconds_diff = seconds(state_latest_, state);
+        if (state.watt_[i] > 0) {
+          const double watt_average =
+              (state_latest_.watt_[i] + state.watt_[i]) / 2.f;
+          state.joules_[i] =
+              state_latest_.joules_[i] + watt_average * seconds_diff;
+        } else if (state.joules_[i] > 0) {
+          const double joules_diff =
+              state.joules_[i] - state_latest_.joules_[i];
+          state.watt_[i] = joules_diff / seconds_diff;
+        }
+      }
+
+      state_latest_ = state;
+
+      if (!thread_started_) {
+        thread_mutex_.unlock();
+        thread_started_ = true;
       }
     }
   });
@@ -159,39 +181,38 @@ void PMT::Mark(const State &state, const std::string &message) const {
   }
 }
 
-void PMT::SetMeasurementInterval(unsigned int milliseconds) {
-  if (milliseconds > 0) {
-    measurement_interval_ = milliseconds;
-  } else {
-    unsigned int measurement_interval = 1;
-    State state_first;
-    State state_second;
-    do {
-      state_first = GetState();
-      std::this_thread::sleep_for(
-          std::chrono::milliseconds(measurement_interval));
-      state_second = GetState();
-      if (!isEqual(state_first.watt_[0], state_second.watt_[0])) {
-        measurement_interval_ = measurement_interval;
-        return;
-      } else {
-        measurement_interval *= 2;
-      }
-    } while (measurement_interval < 1000);
-  }
-}
-
 Timestamp PMT::GetTime() { return std::chrono::system_clock::now(); }
 
 State PMT::Read() {
   const int measurement_interval = GetMeasurementInterval();
   if (!thread_started_) {
     StartThread();
-    thread_started_ = true;
-    std::this_thread::sleep_for(
-        std::chrono::milliseconds(measurement_interval));
+
+    unsigned int retries = 0;
+
+    do {
+      std::this_thread::sleep_for(
+          std::chrono::milliseconds(measurement_interval));
+      if (retries++ > kNrRetriesInitialization) {
+        throw pmt::Exception("Measurement thread failed to start");
+      }
+    } while (!thread_mutex_.try_lock());
+
+    state_returned_ = state_latest_;
+    return state_latest_;
   }
-  return state_latest_;
+
+  State state = state_latest_;
+  if (seconds(state_returned_, state) <= 0) {
+    state = state_returned_;
+    state.timestamp_ = GetTime();
+    const double duration = seconds(state_returned_, state);
+    for (int i = 0; i < state.NrMeasurements(); i++) {
+      state.joules_[i] += state.watt_[i] * duration;
+    }
+  }
+
+  return state_returned_ = state;
 }
 
 std::unique_ptr<PMT> Create(const std::string &name,
@@ -285,9 +306,9 @@ std::unique_ptr<PMT> Create(const std::string &name,
   }
 
 #if defined(DEBUG)
-  std::stringstream error;
-  error << "Invalid or unavailable platform specified: " << name << std::endl;
-  throw std::runtime_error(error.str());
+  std::ostringstream message;
+  message << "Invalid or unavailable platform specified: " << name << std::endl;
+  throw pmt::Exception(message.str().c_str());
 #endif
 
   return Dummy::Create();
diff --git a/common/PMT.h b/common/PMT.h
index 61e79f51eac96f0418898908cb3435a074495361..92003f28febfa000da57cc96751785c3e27ac849 100644
--- a/common/PMT.h
+++ b/common/PMT.h
@@ -1,7 +1,6 @@
-#ifndef PMT_COMMON_H_
-#define PMT_COMMON_H_
+#ifndef PMT_PMT_H_
+#define PMT_PMT_H_
 
-#include <chrono>
 #include <fstream>
 #include <memory>
 #include <mutex>
@@ -9,44 +8,14 @@
 #include <thread>
 #include <vector>
 
+#include <pmt/common/State.h>
+#include <pmt/common/Timestamp.h>
+
 namespace pmt {
 
 const std::string kDumpFilenameVariable = "PMT_DUMP_FILE";
 const std::string kDumpIntervalVariable = "PMT_DUMP_INTERVAL";
 
-using Timestamp = std::chrono::high_resolution_clock::time_point;
-
-class State {
- public:
-  State &operator=(const State &state) {
-    timestamp_ = state.timestamp_;
-    nr_measurements_ = state.nr_measurements_;
-    name_ = state.name_;
-    joules_ = state.joules_;
-    watt_ = state.watt_;
-    return *this;
-  }
-
-  State(int nr_measurements = 1) : nr_measurements_(nr_measurements) {
-    name_.resize(nr_measurements);
-    joules_.resize(nr_measurements);
-    watt_.resize(nr_measurements);
-  }
-
-  int NrMeasurements() { return nr_measurements_; }
-
-  Timestamp timestamp() { return timestamp_; }
-  std::string name(int i);
-  float joules(int i);
-  float watts(int i);
-
-  Timestamp timestamp_;
-  int nr_measurements_;
-  std::vector<std::string> name_;
-  std::vector<float> joules_;
-  std::vector<float> watt_;
-};
-
 class PMT {
  public:
   virtual ~PMT();
@@ -66,7 +35,9 @@ class PMT {
 
   virtual void Mark(const State &state, const std::string &message) const;
 
-  void SetMeasurementInterval(unsigned int milliseconds = 0);
+  void SetMeasurementInterval(unsigned int milliseconds) {
+    measurement_interval_ = milliseconds;
+  };
   unsigned int GetMeasurementInterval() const {
     return measurement_interval_;
   };                               // in milliseconds
@@ -83,24 +54,31 @@ class PMT {
 
   Timestamp GetTime();
 
- private:
-  unsigned int measurement_interval_ = 100;  // milliseconds
+  unsigned int measurement_interval_ = 1;  // milliseconds
+
+  // The last State returned by Read()
+  State state_returned_;
 
-  // The last state set by the thread
+  // The latest State returned by GetState()
   State state_latest_;
 
-  // This thread continuously call GetState to update state_latest_. It is
+ private:
+  // Wait up to this number of measurement intervals for the backend to
+  // initialize, which it signals by unlocking the thread_mutex_.
+  static constexpr int kNrRetriesInitialization = 10;
+
+  // This thread continuously calls GetState to update state_latest_. It is
   // started automatically upon the first Read() call.
   std::thread thread_;
   volatile bool thread_started_ = false;
   volatile bool thread_stop_ = false;
+  mutable std::mutex thread_mutex_;
 
   void StartThread();
   void StopThread();
 
   void DumpHeader(const State &state);
 
- protected:
   std::unique_ptr<std::ofstream> dump_file_ = nullptr;
   mutable std::mutex dump_file_mutex_;
 };
@@ -112,4 +90,4 @@ std::unique_ptr<PMT> Create(const std::string &name,
 
 std::ostream &operator<<(std::ostream &os, const pmt::Timestamp &timestamp);
 
-#endif
+#endif  // PMT_PMT_H_
diff --git a/common/State.cpp b/common/State.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..59f248ff60c0df50823dc5862c1ab41a3e334dfb
--- /dev/null
+++ b/common/State.cpp
@@ -0,0 +1,18 @@
+#include "State.h"
+
+namespace pmt {
+std::string State::name(int i) const {
+  assert(i < nr_measurements_);
+  return name_[i];
+}
+
+float State::joules(int i) const {
+  assert(i < nr_measurements_);
+  return joules_[i];
+}
+
+float State::watts(int i) const {
+  assert(i < nr_measurements_);
+  return watt_[i];
+}
+}  // end namespace pmt
\ No newline at end of file
diff --git a/common/State.h b/common/State.h
new file mode 100644
index 0000000000000000000000000000000000000000..60e2426ed387f3f4ed2aa188230ac0fb970b59c6
--- /dev/null
+++ b/common/State.h
@@ -0,0 +1,49 @@
+#ifndef PMT_STATE_H_
+#define PMT_STATE_H_
+
+#include <cassert>
+#include <string>
+#include <vector>
+
+#include <pmt/common/Timestamp.h>
+
+namespace pmt {
+
+class State {
+ public:
+  State &operator=(const State &state) {
+    timestamp_ = state.timestamp_;
+    nr_measurements_ = state.nr_measurements_;
+    name_ = state.name_;
+    joules_ = state.joules_;
+    watt_ = state.watt_;
+    return *this;
+  }
+
+  State(int nr_measurements = 1) : nr_measurements_(nr_measurements) {
+    name_.resize(nr_measurements);
+    joules_.resize(nr_measurements);
+    watt_.resize(nr_measurements);
+    for (int i = 0; i < nr_measurements; i++) {
+      name_[i] = "";
+      joules_[i] = 0;
+      watt_[i] = 0;
+    }
+  }
+
+  Timestamp timestamp() const { return timestamp_; }
+  int NrMeasurements() const { return nr_measurements_; }
+  std::string name(int i) const;
+  float joules(int i) const;
+  float watts(int i) const;
+
+  Timestamp timestamp_;
+  int nr_measurements_;
+  std::vector<std::string> name_;
+  std::vector<float> joules_;
+  std::vector<float> watt_;
+};
+
+}  // namespace pmt
+
+#endif  // PMT_STATE_H_
\ No newline at end of file
diff --git a/common/Timestamp.h b/common/Timestamp.h
new file mode 100644
index 0000000000000000000000000000000000000000..90e3c93492d4283b034d078394cf6a2b0788b316
--- /dev/null
+++ b/common/Timestamp.h
@@ -0,0 +1,12 @@
+#ifndef PMT_TIMESTAMP_H_
+#define PMT_TIMESTAMP_H_
+
+#include <chrono>
+
+namespace pmt {
+
+using Timestamp = std::chrono::high_resolution_clock::time_point;
+
+}  // end namespace pmt
+
+#endif  // PMT_TIMESTAMP_H_
diff --git a/common/cpu.h b/common/cpu.h
index 470188d60f87b8c635ae923646ba15078ef524af..709cf7cc6142eae5695746f8d26857d20c990e12 100644
--- a/common/cpu.h
+++ b/common/cpu.h
@@ -10,6 +10,8 @@
 
 #include <unistd.h>
 
+#include "common/Exception.h"
+
 namespace pmt::common {
 
 std::vector<int> get_active_cpus() {
@@ -18,8 +20,7 @@ std::vector<int> get_active_cpus() {
 
   const int result = sched_getaffinity(0, sizeof(cpu_set), &cpu_set);
   if (result == -1) {
-    throw std::system_error(errno, std::generic_category(),
-                            "sched_getaffinity");
+    throw pmt::Exception("sched_getaffinity");
   }
 
   const int n_cpus = sysconf(_SC_NPROCESSORS_ONLN);
@@ -44,7 +45,9 @@ std::set<int> get_active_packages(const std::vector<int>& active_cpus) {
 
     std::ifstream file(path);
     if (!file.is_open()) {
-      throw std::runtime_error("Failed to open file: " + path);
+      std::ostringstream message;
+      message << "Failed to open file: " << path;
+      throw pmt::Exception(message.str().c_str());
     }
 
     int socket_id;
diff --git a/cray/Cray.cpp b/cray/Cray.cpp
index 5456a8c311f6e08f9356bd8950e3a3012e5c071c..d691d77ac28c012a8b0dd4da997fd90aceb8f139 100644
--- a/cray/Cray.cpp
+++ b/cray/Cray.cpp
@@ -7,6 +7,7 @@
 
 #include <ext/alloc_traits.h>
 
+#include "common/Exception.h"
 #include "Cray.h"
 #include "FilenamesHelper.h"
 
@@ -14,19 +15,19 @@ namespace {
 double GetPower(const std::string& filePath) {
   std::ifstream powerFile(filePath);
   if (!powerFile.is_open()) {
-    throw std::runtime_error("Failed to open power file");
+    throw pmt::Exception("Failed to open power file");
   }
 
   std::string line;
   if (!std::getline(powerFile, line)) {
-    throw std::runtime_error("Failed to read power value");
+    throw pmt::Exception("Failed to read power value");
   }
 
   double power;
   try {
     power = std::stod(line);
   } catch (const std::exception& e) {
-    throw std::runtime_error("Failed to parse power value");
+    throw pmt::Exception("Failed to parse power value");
   }
 
   powerFile.close();
@@ -46,9 +47,6 @@ class CrayImpl : public Cray {
   std::vector<std::string> filenames;
   std::string cray_pm_counters_path = "/sys/cray/pm_counters";
 
-  Timestamp previous_timestamp_;
-  std::vector<CrayMeasurement> previous_measurements_;
-
   // Mutex used to guard GetMeasurements()
   std::mutex mutex_;
 
@@ -67,9 +65,6 @@ CrayImpl::CrayImpl() {
 #if defined(DEBUG)
   filenames_helper::PrintFilenames(filenames);
 #endif
-
-  previous_timestamp_ = GetTime();
-  previous_measurements_ = GetMeasurements();
 }
 
 std::vector<CrayMeasurement> CrayImpl::GetMeasurements() {
@@ -95,14 +90,11 @@ State CrayImpl::GetState() {
   State state(measurements.size());
   state.timestamp_ = GetTime();
 
-  const double duration = seconds(previous_timestamp_, state.timestamp_);
+  const double duration = seconds(state_latest_, state);
 
   for (size_t i = 0; i < measurements.size(); i++) {
     state.name_[i] = measurements[i].name;
     state.watt_[i] = measurements[i].watt;
-    const double watt =
-        (measurements[i].watt + previous_measurements_[i].watt) / 2;
-    state.joules_[i] += watt * duration;
   }
 
   return state;
diff --git a/dummy/Dummy.cpp b/dummy/Dummy.cpp
index 9a0544944ce8767c0d5c31c9505ead952cf45b05..bfbfabcd7f43899fbcd7a35756d3e267cf3bbefe 100644
--- a/dummy/Dummy.cpp
+++ b/dummy/Dummy.cpp
@@ -1,4 +1,3 @@
-#include <ext/alloc_traits.h>
 #include <vector>
 
 #include "Dummy.h"
@@ -14,12 +13,48 @@ class DummyImpl : public Dummy {
 
 std::unique_ptr<Dummy> Dummy::Create() { return std::make_unique<DummyImpl>(); }
 
+// Returns a State object containing:
+// - timestamp: current system time
+// - name[0]: "total" for accumulated values
+// - name[1..n]: "sensor0".."sensorN" for individual sensors
+//
+// Operates in three modes:
+// 1. Default: report zero power and energy (wattt_.size() == 0)
+//
+// 2. Fixed joules (fixed_joules_ == true):
+//    - Accumulates energy based on fixed power values
+//    - Skips accumulation on first measurement
+//    - joules[0]: sum of all sensors
+//    - joules[1..n]: individual sensor values
+//
+// 3. Fixed watts (fixed_joules_ == false):
+//    - Reports constant power values
+//    - watt[0]: sum of all sensors
+//    - watt[1..n]: individual sensor values
 State DummyImpl::GetState() {
-  State state;
+  State state(1 + watt_.size());
   state.timestamp_ = PMT::GetTime();
-  state.name_[0] = "none";
-  state.joules_[0] = 0;
-  state.watt_[0] = 0;
+  state.name_[0] = "total";
+  if (fixed_joules_) {
+    // Accumulate joules as if the device consumed watt_ watts over the previous
+    // measurement interval, skipping the first measurement where the previous
+    // timestamp is zero.
+    if (state_latest_.timestamp_.time_since_epoch().count()) {
+      for (size_t i = 0; i < watt_.size(); i++) {
+        state.name_[i] = "sensor" + std::to_string(i);
+        state.joules_[i + 1] = state_latest_.joules_[i + 1] +
+                               watt_[i] * PMT::seconds(state_latest_, state);
+        state.joules_[0] += state.joules_[i + 1];
+      }
+    }
+  } else {
+    // Use a fixed wattage
+    for (size_t i = 0; i < watt_.size(); i++) {
+      state.name_[i] = "sensor" + std::to_string(i);
+      state.watt_[0] += watt_[i];
+      state.watt_[i + 1] = watt_[i];
+    }
+  }
   return state;
 }
 
diff --git a/dummy/Dummy.h b/dummy/Dummy.h
index 4941ac5f911a10fd2b2e223452474f4a7851e176..9718c5c6baf0ba44297887107c444f3070b3770e 100644
--- a/dummy/Dummy.h
+++ b/dummy/Dummy.h
@@ -9,6 +9,15 @@ namespace pmt {
 class Dummy : public PMT {
  public:
   static std::unique_ptr<Dummy> Create();
+  void UseFixedWatts(const std::vector<float>& watt) { watt_ = watt; }
+  void UseFixedJoules(const std::vector<float>& watt) {
+    fixed_joules_ = true;
+    watt_ = watt;
+  }
+
+ protected:
+  bool fixed_joules_ = false;
+  std::vector<float> watt_;
 };
 }  // end namespace pmt
 
diff --git a/likwid/LikwidImpl.cpp b/likwid/LikwidImpl.cpp
index 43e233723d9285ca7ef30187b3357a2446f33c80..de317948ac2b5b8c61e38907a1e16ade38de7f7e 100644
--- a/likwid/LikwidImpl.cpp
+++ b/likwid/LikwidImpl.cpp
@@ -106,8 +106,6 @@ LikwidImpl::LikwidImpl(std::string event_group_name)
       }
     }
   }
-
-  previous_time = GetTime();
 }  // end constructor
 
 LikwidImpl::~LikwidImpl() {
@@ -118,22 +116,17 @@ LikwidImpl::~LikwidImpl() {
 
 State LikwidImpl::GetState() {
   State state(nr_groups + 1);
+  state.name_[0] = "total";
   state.timestamp_ = GetTime();
 
-  const double duration = seconds(state.timestamp_, state.timestamp_);
-  double total_watts = 0;
+  const double duration = seconds(state_latest_, state);
   std::vector<double> current_measurements = GetMeasurements();
   for (int i = 0; i < nr_groups; i++) {
-    joulesTotal += current_measurements[i];
-    state.joules_[i + 1] = current_measurements[i];
+    state.name_[i] = "socket_" + std::to_string(i);
     state.watt_[i + 1] = current_measurements[i] / duration;
-    total_watts += state.watt_[i + 1];
+    state.watt_[0] += state.watt_[i + 1];
   }
 
-  state.joules_[0] = static_cast<float>(joulesTotal);
-  state.watt_[0] = total_watts;
-  previous_time = state.timestamp_;
-
   return state;
 }
 
diff --git a/likwid/LikwidImpl.h b/likwid/LikwidImpl.h
index c82966e78604c1e50ecd4e825b7b0ae31358203a..c31c307024441ba8694745cd7cc3dd09a143c305 100644
--- a/likwid/LikwidImpl.h
+++ b/likwid/LikwidImpl.h
@@ -24,7 +24,6 @@ class LikwidImpl : public Likwid {
   std::string event_group;
   std::vector<double> GetMeasurements();
 
-  Timestamp previous_time;
   int nr_groups, nr_events;
   int nr_threads_group;
   std::vector<int> relevant_event_ids;
diff --git a/nvidia/NVIDIA.cpp b/nvidia/NVIDIA.cpp
index 9479d8858fd2ee000383479f34624abe2f472491..f99a83e0ed8790bbf72a520491cc04464b15424f 100644
--- a/nvidia/NVIDIA.cpp
+++ b/nvidia/NVIDIA.cpp
@@ -16,13 +16,13 @@
 inline void __checkCudaCall(cudaError_t result, const char *const func,
                             const char *const file, int const line) {
   if (result != cudaSuccess) {
-    std::stringstream error;
-    error << "CUDA Error at " << file;
-    error << ":" << line;
-    error << " in function " << func;
-    error << ": " << cudaGetErrorString(result);
-    error << std::endl;
-    throw std::runtime_error(error.str());
+    std::ostringstream message;
+    message << "CUDA Error at " << file;
+    message << ":" << line;
+    message << " in function " << func;
+    message << ": " << cudaGetErrorString(result);
+    message << std::endl;
+    throw std::runtime_error(message.str());
   }
 }
 #endif
diff --git a/nvml/NVMLImpl.cpp b/nvml/NVMLImpl.cpp
index 1bfd60d8b4effdc4a97108c0dbae197022b706c6..5cac5a69eeb7a42566016742c465bb2a3cb3ec54 100644
--- a/nvml/NVMLImpl.cpp
+++ b/nvml/NVMLImpl.cpp
@@ -9,20 +9,10 @@
 #include <ext/alloc_traits.h>
 
 #include "NVMLImpl.h"
+#include "common/Exception.h"
 
 namespace pmt::nvml {
 
-NVMLState::operator State() {
-  State state(measurements_.size());
-  state.timestamp_ = timestamp_;
-  state.joules_[0] = joules_;
-  for (size_t i = 0; i < measurements_.size(); i++) {
-    state.name_[i] = measurements_[i].name_;
-    state.watt_[i] = measurements_[i].milliwatt_ / 1.e3;
-  }
-  return state;
-}
-
 NVMLImpl::NVMLImpl(int device_number) {
   const char *pmt_device = getenv("PMT_DEVICE");
   device_number = pmt_device ? atoi(pmt_device) : device_number;
@@ -54,13 +44,8 @@ void NVMLImpl::Initialize() {
   device_->getFieldValues(1, values);
   nr_scopes_ = 1 + (values[0].nvmlReturn == NVML_SUCCESS);
 #endif
-
-  // Initialize the first timestamp
-  state_previous_.timestamp_ = GetTime();
 }
 
-NVMLImpl::~NVMLImpl() { stopped_ = true; }
-
 #if defined(PMT_NVML_LEGACY_MODE)
 std::vector<NVMLMeasurement> NVMLImpl::GetMeasurements() {
   return {{.name_ = "gpu_average",
@@ -103,44 +88,33 @@ std::vector<NVMLMeasurement> NVMLImpl::GetMeasurements() {
 }
 #endif
 
-NVMLState NVMLImpl::GetNVMLState() {
-  if (stopped_) {
-    return state_previous_;
+State NVMLImpl::GetState() {
+  std::vector<NVMLMeasurement> measurements;
+  try {
+    measurements = GetMeasurements();
+  } catch (const ::nvml::Error &e) {
+    throw pmt::Exception(e.what());
   }
 
-  NVMLState state;
-  try {
-    state.measurements_ = GetMeasurements();
+  State state(measurements.size());
 
-    // Default: use the instantaneous GPU power
-    // Grace Hopper: use the instantaneous module power
-#if defined(PMT_NVML_LEGACY_MODE)
-    state.milliwatt_ = state.measurements_[0].milliwatt_;
-    state.timestamp_ = state.measurements_[0].timestamp_;
+  for (size_t i = 0; i < measurements.size(); i++) {
+    state.name_[i] = measurements[i].name_;
+    state.joules_[i] = 0;
+    state.watt_[i] = measurements[i].milliwatt_ / 1.e3;
+  }
+
+#if !defined(PMT_NVML_LEGACY_MODE)
+  // Default: use the instantaneous GPU power
+  // Grace Hopper: use the instantaneous module power
+  const unsigned int measurement_id = nr_scopes_ == 1 ? 0 : 2;
+  std::swap(state.name_[0], state.name_[measurement_id]);
+  std::swap(state.watt_[0], state.watt_[measurement_id]);
+  state.timestamp_ = measurements[measurement_id].timestamp_;
 #else
-    const unsigned int measurement_id = nr_scopes_ == 1 ? 0 : 2;
-    state.milliwatt_ = state.measurements_[measurement_id].milliwatt_;
-    state.timestamp_ = state.measurements_[measurement_id].timestamp_;
+  state.timestamp_ = measurements[0].timestamp_;
 #endif
 
-    // Set derived fields of state
-    const double duration =
-        seconds(state_previous_.timestamp_, state.timestamp_);
-
-    if (state_previous_.joules_ > 0) {
-      const double watt =
-          (state.milliwatt_ + state_previous_.milliwatt_) / 2.e3;
-      state.joules_ = state_previous_.joules_ + watt * duration;
-    } else {
-      const double watt = state.milliwatt_ / 1.e3;
-      state.joules_ = watt * duration;
-    }
-
-    state_previous_ = state;
-  } catch (std::runtime_error &e) {
-    return state_previous_;
-  }
-
   return state;
 }
 }  // end namespace pmt::nvml
diff --git a/nvml/NVMLImpl.h b/nvml/NVMLImpl.h
index 74b2ffd81b12959ca6ba37da3178408f7918fde2..46db4d1b3b6f4676dd2c2cf5b813d2580bd1a6db 100644
--- a/nvml/NVMLImpl.h
+++ b/nvml/NVMLImpl.h
@@ -18,10 +18,7 @@ struct NVMLMeasurement {
 class NVMLState {
  public:
   operator State();
-  Timestamp timestamp_;
   std::vector<NVMLMeasurement> measurements_;
-  unsigned int milliwatt_ = 0;
-  unsigned int joules_ = 0;
 };
 
 class NVMLImpl : public NVML {
@@ -30,16 +27,13 @@ class NVMLImpl : public NVML {
 #if defined(PMT_NVML_CUDAWRAPPERS_API)
   NVMLImpl(cu::Device& device);
 #endif
-  ~NVMLImpl();
 
  private:
   void Initialize();
-  State GetState() override { return GetNVMLState(); }
+  State GetState() override;
 
   virtual const char* GetDumpFilename() override { return "/tmp/pmt_nvml.out"; }
 
-  NVMLState state_previous_;
-  NVMLState GetNVMLState();
   std::vector<NVMLMeasurement> GetMeasurements();
 
 #if not defined(PMT_NVML_LEGACY_MODE)
@@ -47,7 +41,6 @@ class NVMLImpl : public NVML {
   const unsigned int kFieldIdPowerAverage = NVML_FI_DEV_POWER_AVERAGE;
   unsigned int nr_scopes_;
 #endif
-  bool stopped_ = false;
 
   std::unique_ptr<::nvml::Context> context_;
   std::unique_ptr<::nvml::Device> device_;
diff --git a/powersensor3/PowerSensor3Impl.cpp b/powersensor3/PowerSensor3Impl.cpp
index ff37be00df2bc30408e2f59dc7d8a6a2302d98a9..6d5f59affa9f7a96971fdb09db8a11c323102fe6 100644
--- a/powersensor3/PowerSensor3Impl.cpp
+++ b/powersensor3/PowerSensor3Impl.cpp
@@ -2,19 +2,6 @@
 
 namespace pmt::powersensor3 {
 
-PowerSensor3State::operator State() {
-  State state(::PowerSensor3::MAX_PAIRS);
-  state.timestamp_ = state_.timeAtRead;
-
-  for (size_t i = 0; i < state.nr_measurements_; i++) {
-    state.name_[i] = pair_names_[i];
-    state.watt_[i] = state_.voltage[i] * state_.current[i];
-    state.joules_[i] = state_.consumedEnergy[i];
-  }
-
-  return state;
-}
-
 PowerSensor3Impl::PowerSensor3Impl(const char *device)
     : powersensor_(std::make_unique<::PowerSensor3::PowerSensor>(device)) {
   for (unsigned pair_id = 0; pair_id < ::PowerSensor3::MAX_PAIRS; pair_id++) {
@@ -22,19 +9,18 @@ PowerSensor3Impl::PowerSensor3Impl(const char *device)
   }
 }
 
-PowerSensor3Impl::~PowerSensor3Impl() { stopped_ = true; }
+State PowerSensor3Impl::GetState() {
+  const ::PowerSensor3::State measurement = powersensor_->read();
 
-PowerSensor3State PowerSensor3Impl::GetPowerSensor3State() {
-  if (stopped_) {
-    return state_previous_;
-  }
+  State state(::PowerSensor3::MAX_PAIRS);
+  state.name_ = pair_names_;
+  state.timestamp_ = measurement.timeAtRead;
 
-  try {
-    const PowerSensor3State state{powersensor_->read(), pair_names_};
-    state_previous_ = state;
-    return state;
-  } catch (std::runtime_error &e) {
-    return state_previous_;
+  for (size_t i = 0; i < state.nr_measurements_; i++) {
+    state.watt_[i] = measurement.voltage[i] * measurement.current[i];
+    state.joules_[i] = measurement.consumedEnergy[i];
   }
+
+  return state;
 }
 }  // end namespace pmt::powersensor3
diff --git a/powersensor3/PowerSensor3Impl.h b/powersensor3/PowerSensor3Impl.h
index 731714a6bb249a2704be74329ab61b84f604db6f..b07813cb8b8604604e2e603d3b5ae1fc0b23ac61 100644
--- a/powersensor3/PowerSensor3Impl.h
+++ b/powersensor3/PowerSensor3Impl.h
@@ -8,30 +8,17 @@
 
 namespace pmt::powersensor3 {
 
-class PowerSensor3State {
- public:
-  operator State();
-  ::PowerSensor3::State state_;
-  std::vector<std::string> pair_names_;
-};
-
 class PowerSensor3Impl : public PowerSensor3 {
  public:
   PowerSensor3Impl(const char *device);
-  ~PowerSensor3Impl();
 
  private:
-  State GetState() override { return GetPowerSensor3State(); }
+  State GetState() override;
 
   virtual const char *GetDumpFilename() override {
     return "/tmp/pmt_powersensor3.out";
   }
 
-  PowerSensor3State state_previous_;
-  PowerSensor3State GetPowerSensor3State();
-
-  bool stopped_ = false;
-
   std::unique_ptr<::PowerSensor3::PowerSensor> powersensor_;
   std::vector<std::string> pair_names_;
 };
diff --git a/rapl/RaplImpl.cpp b/rapl/RaplImpl.cpp
index 58ecba3cbeadcceda5373104a742d1d19dcf4ce3..05889f057de2712f85254f44c90c8997becfb340 100644
--- a/rapl/RaplImpl.cpp
+++ b/rapl/RaplImpl.cpp
@@ -40,16 +40,12 @@ namespace fs = std::filesystem;
 
 namespace pmt::rapl {
 
-RaplImpl::RaplImpl() {
-  Init();
-  previous_timestamp_ = GetTime();
-  previous_measurements_ = GetMeasurements();
-}
+RaplImpl::RaplImpl() { Init(); }
 
 RaplImpl::~RaplImpl() { std::lock_guard<std::mutex> lock(mutex_); }
 
 std::string GetBasename(int package_id) {
-  std::stringstream basename;
+  std::ostringstream basename;
   basename << "/sys/class/powercap/intel-rapl:" << package_id;
   return basename.str();
 }
@@ -91,7 +87,7 @@ void RaplImpl::Init() {
     try {
       rapl_counters_.emplace_back(rapl_dir);
     } catch (std::system_error& e) {
-      std::stringstream message;
+      std::ostringstream message;
       message << "OS error: " << e.what();
       std::cerr << message.str() << std::endl;
       if (e.code().value() == EACCES) {
@@ -118,29 +114,16 @@ State RaplImpl::GetState() {
   State state(1 + measurements.size());
   state.timestamp_ = GetTime();
   state.name_[0] = "total";
-  state.joules_[0] = 0;
-  state.watt_[0] = 0;
 
   for (std::size_t i = 0; i < measurements.size(); i++) {
-    const std::string name = measurements[i].name;
-    const std::size_t ujoules_now = measurements[i].ujoules;
-    const std::size_t ujoules_previous = previous_measurements_[i].ujoules;
-    const double duration = seconds(previous_timestamp_, state.timestamp_);
-    const float joules_diff = (ujoules_now - ujoules_previous) * 1e-6;
-    const float watt = joules_diff / duration;
-    state.name_[i + 1] = name;
-    state.joules_[i + 1] = ujoules_now * 1e-6;
-    state.watt_[i + 1] = watt;
-
-    if (name.find("package") != std::string::npos) {
-      state.joules_[0] += ujoules_now * 1e-6;
-      state.watt_[0] += watt;
+    state.name_[i + 1] = measurements[i].name;
+    state.joules_[i + 1] = measurements[i].ujoules * 1e-6;
+
+    if (measurements[i].name.find("package") != std::string::npos) {
+      state.joules_[0] += measurements[i].ujoules * 1e-6;
     }
   }
 
-  previous_timestamp_ = state.timestamp_;
-  previous_measurements_ = measurements;
-
   return state;
 }
 
diff --git a/rapl/RaplImpl.h b/rapl/RaplImpl.h
index b5c1d679a2bced0c930e188307945bc626fde9a6..49ee3cd517871a0ebd6cdc5201da1989909c9e5f 100644
--- a/rapl/RaplImpl.h
+++ b/rapl/RaplImpl.h
@@ -32,8 +32,6 @@ class RaplImpl : public Rapl {
 
   std::vector<RaplMeasurement> GetMeasurements();
 
-  Timestamp previous_timestamp_;
-  std::vector<RaplMeasurement> previous_measurements_;
   std::vector<RaplCounter> rapl_counters_;
 
   // Mutex used to guard GetMeasurements()
diff --git a/rocm/ROCMImpl.cpp b/rocm/ROCMImpl.cpp
index 0601d1bb73025e0c92f270fc7e27774070b5647c..384040415eff6bdc05732b29c22ba54e9bffa4b0 100644
--- a/rocm/ROCMImpl.cpp
+++ b/rocm/ROCMImpl.cpp
@@ -12,11 +12,10 @@ class RocmError : public std::exception {
       : result_(result), file_(file), line_(line) {}
 
   const char *what() const noexcept override {
-    std::ostringstream oss;
-    oss << "RSMI call failed with status: " << result_ << " in " << file_
-        << " at line " << line_;
-    message_ = oss.str();
-    return message_.c_str();
+    std::ostringstream message;
+    message << "RSMI call failed with status: " << result_ << " in " << file_
+            << " at line " << line_;
+    return message.str().c_str();
   }
 
   operator rsmi_status_t() const { return result_; }
@@ -25,7 +24,6 @@ class RocmError : public std::exception {
   rsmi_status_t result_;
   const char *file_;
   int line_;
-  mutable std::string message_;
 };
 
 inline void checkRsmiCall(rsmi_status_t result, const char *file, int line) {
@@ -56,34 +54,16 @@ ROCMImpl::ROCMImpl(const unsigned device_number) {
   checkRsmiCall(rsmi_init(0));
 
   device_number_ = device_number;
-
-  state_previous_ = GetROCMState();
-  state_previous_.joules_ = 0;
 }
 
 ROCMImpl::~ROCMImpl() { checkRsmiCall(rsmi_shut_down()); }
 
-ROCMState::operator State() {
+State ROCMImpl::GetState() {
   State state;
-  state.timestamp_ = timestamp_;
-  state.name_[0] = "device";
-  state.joules_[0] = joules_;
-  state.watt_[0] = watt_;
-  return state;
-}
-
-ROCMState ROCMImpl::GetROCMState() {
-  ROCMState state;
   state.timestamp_ = GetTime();
-  state.watt_ = GetPower(device_number_);
-  state.joules_ = state_previous_.joules_;
-  const float watt = (state.watt_ + state_previous_.watt_) / 2;
-  const double duration = seconds(state_previous_.timestamp_, state.timestamp_);
-  state.joules_ += watt * duration;
-  state_previous_ = state;
+  state.name_[0] = "device";
+  state.watt_[0] = GetPower(device_number_);
   return state;
 }
 
-State ROCMImpl::GetState() { return GetROCMState(); }
-
 }  // end namespace pmt::rocm
diff --git a/rocm/ROCMImpl.h b/rocm/ROCMImpl.h
index 9ce4069a7d7a45a47df66be383681cb9d8aeffe9..a22bc0a564ff557ec0bc8dc942621ba01eb4d885 100644
--- a/rocm/ROCMImpl.h
+++ b/rocm/ROCMImpl.h
@@ -3,14 +3,6 @@
 #include "ROCM.h"
 
 namespace pmt::rocm {
-class ROCMState {
- public:
-  operator State();
-  Timestamp timestamp_;
-  double watt_ = 0;
-  double joules_ = 0;
-};
-
 class ROCMImpl : public ROCM {
  public:
   ROCMImpl(const unsigned device_number);
@@ -22,8 +14,5 @@ class ROCMImpl : public ROCM {
   virtual const char *GetDumpFilename() override { return "/tmp/pmt_rocm.out"; }
 
   unsigned int device_number_;
-
-  ROCMState state_previous_;
-  ROCMState GetROCMState();
 };
 }  // end namespace pmt::rocm
\ No newline at end of file
diff --git a/tegra/TegraImpl.cpp b/tegra/TegraImpl.cpp
index c6d61e45b5b1b5bbb08e3835a67940a06e45e7fa..a67fbb67bcb5b1fe5ad6fb64c8993df4eda3d7e7 100644
--- a/tegra/TegraImpl.cpp
+++ b/tegra/TegraImpl.cpp
@@ -13,9 +13,10 @@
 #include <ext/alloc_traits.h>
 #include <signal.h>
 
+#include "common/Exception.h"
 #include "TegraImpl.h"
 
-namespace detail {
+namespace {
 bool FileExists(const std::string& name) {
   if (FILE* file = fopen(name.c_str(), "r")) {
     fclose(file);
@@ -31,7 +32,7 @@ std::string Execute(const std::string& commandline) {
   std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(commandline.c_str(), "r"),
                                                 pclose);
   if (!pipe) {
-    throw std::runtime_error("popen() failed!");
+    throw pmt::Exception("popen() failed!");
   }
   while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
     result += buffer.data();
@@ -78,15 +79,17 @@ std::string FindLogfile() {
 std::string StartTegraStats(int interval) {
   char filename[32] = "/tmp/tegrastats-XXXXXX";
   if (mkstemp(filename) == -1) {
-    throw std::runtime_error("Could not create temporary file");
+    throw pmt::Exception("Could not create temporary file");
   }
   const char* binary = "/usr/bin/tegrastats";
-  std::stringstream commandline;
+  std::ostringstream commandline;
   commandline << binary << " --start --interval " << interval << " --logfile "
               << filename;
   std::string output = Execute(commandline.str());
   if (output.compare("") != 0) {
-    throw std::runtime_error("Error starting tegrastats: " + output);
+    std::ostringstream message;
+    message << "Error starting tegrastats: " << output;
+    throw pmt::Exception(message.str().c_str());
   }
   return filename;
 }
@@ -94,7 +97,7 @@ std::string StartTegraStats(int interval) {
 void StopTegraStats(const std::string& logfile) {
   if (logfile.compare("") != 0) {
     const char* binary = "/usr/bin/tegrastats";
-    std::stringstream commandline;
+    std::ostringstream commandline;
     commandline << binary << " --stop";
     Execute(commandline.str());
     std::remove(logfile.c_str());
@@ -145,8 +148,9 @@ std::vector<pmt::tegra::TegraMeasurement> ReadPowerMeasurements(
   return measurements;
 }
 
-bool CheckSensors(const std::vector<std::string>& sensors,
-                  std::vector<pmt::tegra::TegraMeasurement>& measurements) {
+bool CheckSensors(
+    const std::vector<std::string>& sensors,
+    const std::vector<pmt::tegra::TegraMeasurement>& measurements) {
   if (sensors.size() != measurements.size()) {
     return false;
   }
@@ -158,47 +162,44 @@ bool CheckSensors(const std::vector<std::string>& sensors,
   }
   return true;
 }
-}  // end namespace detail
+}  // end namespace
 
 namespace pmt::tegra {
 
 void SignalCallbackHandler(int num) {
-  const std::string logfile = detail::FindLogfile();
-  detail::StopTegraStats(logfile);
+  const std::string logfile = ::FindLogfile();
+  ::StopTegraStats(logfile);
   signal(SIGINT, SIG_DFL);
   raise(num);
 }
 
 TegraImpl::TegraImpl() {
-  filename_ = detail::FindLogfile();
-  if (!detail::FileExists(filename_)) {
-    detail::StopTegraStats(filename_);
-    filename_ = detail::StartTegraStats(measurement_interval_);
+  filename_ = ::FindLogfile();
+  if (!::FileExists(filename_)) {
+    ::StopTegraStats(filename_);
+    filename_ = ::StartTegraStats(measurement_interval_);
     started_tegrastats_ = true;
     signal(SIGINT, SignalCallbackHandler);
   }
-
-  previous_state_ = GetTegraState();
-  previous_state_.joules_ = 0;
 }
 
 TegraImpl::~TegraImpl() {
   if (started_tegrastats_) {
-    detail::StopTegraStats(filename_);
+    ::StopTegraStats(filename_);
     started_tegrastats_ = false;
   }
 }
 
 std::vector<TegraMeasurement> TegraImpl::GetMeasurements() {
-  std::string line = detail::ReadLastLine(filename_);
+  std::string line = ::ReadLastLine(filename_);
 
   while (line.compare("") == 0) {
     std::this_thread::sleep_for(
         std::chrono::milliseconds(measurement_interval_));
-    line = detail::ReadLastLine(filename_);
+    line = ::ReadLastLine(filename_);
   }
 
-  return detail::ReadPowerMeasurements(line);
+  return ::ReadPowerMeasurements(line);
 };
 
 const std::vector<std::string> sensors_agx_xavier{"GPU", "CPU",   "SOC",
@@ -210,55 +211,41 @@ const std::vector<std::string> sensors_jetson_nano{"POM_5V_IN", "POM_5V_GPU",
 const std::vector<std::string> sensors_jetson_orin_nano{
     "VDD_IN", "VDD_CPU_GPU_CV", "VDD_SOC"};
 
-TegraState::operator State() {
+State TegraImpl::GetState() {
+  const std::vector<TegraMeasurement> measurements = GetMeasurements();
   State state(1 + measurements.size());
-  state.timestamp_ = timestamp_;
-  state.name_[0] = "total";
-  state.joules_[0] = joules_ * 1e-3;
-  state.watt_[0] = watt_ * 1e-3;
-
-  for (size_t i = 0; i < measurements.size(); i++) {
-    const std::string name = measurements[i].first;
-    const double watt = double(measurements[i].second) / 1000;
-    state.name_[i + 1] = name;
-    state.watt_[i + 1] = watt;
-  }
-
-  return state;
-}
-
-TegraState TegraImpl::GetTegraState() {
-  TegraState state;
   state.timestamp_ = GetTime();
-  state.measurements = GetMeasurements();
+  state.name_[0] = "total";
 
   // Compute total power consumption as sum of individual measurements.
   // Which individual measurements to use differs per platform.
-  state.watt_ = 0;
-  if (detail::CheckSensors(sensors_agx_xavier, state.measurements)) {
+  state.watt_[0] = 0;
+  if (::CheckSensors(sensors_agx_xavier, measurements)) {
     // Jetson AGX Xavier: sum all sensors
-    for (auto& measurement : state.measurements) {
-      state.watt_ += measurement.second;
+    for (auto& measurement : measurements) {
+      state.watt_[0] += measurement.second;
     }
-  } else if (detail::CheckSensors(sensors_agx_orin, state.measurements)) {
+  } else if (::CheckSensors(sensors_agx_orin, measurements)) {
     // Jetson AGX Orin: sum all sensors
-    for (auto& measurement : state.measurements) {
-      state.watt_ += measurement.second;
+    for (auto& measurement : measurements) {
+      state.watt_[0] += measurement.second;
     }
-  } else if (detail::CheckSensors(sensors_jetson_nano, state.measurements)) {
+  } else if (::CheckSensors(sensors_jetson_nano, measurements)) {
     // Jetson Nano: POM_5V_IN only
-    state.watt_ += state.measurements[0].second;
-  } else if (detail::CheckSensors(sensors_jetson_orin_nano,
-                                  state.measurements)) {
+    state.watt_[0] += measurements[0].second;
+  } else if (::CheckSensors(sensors_jetson_orin_nano, measurements)) {
     // Jetson Nano: VDD_IN only
-    state.watt_ += state.measurements[0].second;
+    state.watt_[0] += measurements[0].second;
+  } else {
+    throw pmt::Exception("Unknown Jetson device.");
   }
-
-  const float watt = (state.watt_ + previous_state_.watt_) / 2;
-  const double duration = seconds(previous_state_.timestamp_, state.timestamp_);
-  state.joules_ = previous_state_.joules_ + (watt * duration);
-
-  previous_state_ = state;
+  for (size_t i = 0; i < measurements.size(); i++) {
+    const std::string name = measurements[i].first;
+    const double watt = measurements[i].second / 1.e3;
+    state.name_[i + 1] = name;
+    state.watt_[i + 1] = watt;
+  }
+  state.watt_[0] /= 1e3;
 
   return state;
 }
diff --git a/tegra/TegraImpl.h b/tegra/TegraImpl.h
index a9132ac5cd05d541755985e9bb72720c85222c46..11e83bc86578fc9ac2b12cc359b10490f60b2911 100644
--- a/tegra/TegraImpl.h
+++ b/tegra/TegraImpl.h
@@ -1,6 +1,4 @@
-#include <algorithm>
 #include <string>
-#include <utility>
 #include <vector>
 
 #include "Tegra.h"
@@ -10,34 +8,23 @@ namespace pmt::tegra {
 
 using TegraMeasurement = std::pair<std::string, int>;
 
-class TegraState {
- public:
-  operator State();
-  Timestamp timestamp_;
-  std::vector<TegraMeasurement> measurements;
-  unsigned int watt_ = 0;
-  unsigned int joules_ = 0;
-};
-
 class TegraImpl : public Tegra {
  public:
   TegraImpl();
   virtual ~TegraImpl();
 
-  State GetState() override { return GetTegraState(); }
+  State GetState() override;
 
   virtual const char *GetDumpFilename() override {
     return "/tmp/pmt_tegra.out";
   }
 
  private:
-  TegraState GetTegraState();
   std::vector<TegraMeasurement> GetMeasurements();
 
   std::string filename_ = "";
   bool started_tegrastats_ = false;
   const int measurement_interval_ = 10;  // milliseconds
-  TegraState previous_state_;
 };
 
 }  // end namespace pmt::tegra
\ No newline at end of file
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..fe3345d22d77c1d3efd6c0f496a83f7698234031
--- /dev/null
+++ b/test/CMakeLists.txt
@@ -0,0 +1,12 @@
+include(FetchContent)
+
+FetchContent_Declare(
+  Catch2
+  GIT_REPOSITORY https://github.com/catchorg/Catch2.git
+  GIT_TAG v3.8.0)
+
+FetchContent_MakeAvailable(Catch2)
+list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/contrib)
+include(Catch)
+
+add_subdirectory(dummy)
diff --git a/test/dummy/CMakeLists.txt b/test/dummy/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..782a0b81c2b5f0df0bf675657b0db5d8a31bba8f
--- /dev/null
+++ b/test/dummy/CMakeLists.txt
@@ -0,0 +1,7 @@
+project(test-dummy)
+
+add_executable(${PROJECT_NAME} ${PROJECT_NAME}.cpp)
+target_link_libraries(${PROJECT_NAME} pmt Catch2::Catch2WithMain)
+target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_BINARY_DIR}
+                                                   ${CMAKE_SOURCE_DIR})
+add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME})
diff --git a/test/dummy/test-dummy.cpp b/test/dummy/test-dummy.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3d94e61f1bddb8f10651e8daec07a172fb9a9b1d
--- /dev/null
+++ b/test/dummy/test-dummy.cpp
@@ -0,0 +1,148 @@
+#include <numeric>
+#include <vector>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/generators/catch_generators.hpp>
+#include <catch2/matchers/catch_matchers_floating_point.hpp>
+
+#include <pmt.h>
+
+#include "dummy/Dummy.h"
+
+/**
+ * Verify default behavior of the Dummy sensor
+ *
+ * Test validates that:
+ * 1. Default sensor returns measurements with zero values
+ * 2. Time advances between measurements
+ * 3. Power and energy calculations return zero
+ */
+TEST_CASE("Default") {
+  const size_t kSleepMs = GENERATE(1, 1e2, 1e3);
+
+  // Initialize
+  auto sensor = pmt::Dummy::Create();
+
+  // Take two measurements
+  pmt::State state1 = sensor->Read();
+  std::this_thread::sleep_for(std::chrono::milliseconds(kSleepMs));
+  pmt::State state2 = sensor->Read();
+
+  // Verify each state has expected default values
+  for (const pmt::State& state : {state1, state2}) {
+    REQUIRE(state.NrMeasurements() == 1);  // Single measurement point
+    REQUIRE(state.watts(0) == 0);          // Zero power consumption
+    REQUIRE(state.joules(0) == 0);         // Zero energy consumption
+  }
+
+  // Verify time-based calculations
+  CHECK(pmt::PMT::seconds(state1, state2) > 0);  // Time advances
+  CHECK_THAT(pmt::PMT::watts(state1, state2),    // Power remains zero
+             Catch::Matchers::WithinULP(0.f, 0));
+  CHECK_THAT(pmt::PMT::joules(state1, state2),  // Energy remains zero
+             Catch::Matchers::WithinULP(0.f, 0));
+}
+
+/**
+ * Verify the Dummy sensor reporting power
+ * - The Dummy sensor is configured to report a fixed power value
+ *
+ * Test validates that:
+ * 1. Sensor correctly reports configured power value
+ * 2. Power calculation from energy matches configured value
+ */
+TEST_CASE("Set watt") {
+  const float kWatt = 42;
+  const size_t kSleepMs = GENERATE(1, 1e2, 1e3);
+
+  // Initialize
+  auto sensor = pmt::Dummy::Create();
+  sensor->UseFixedWatts({kWatt});
+
+  // Take two measurements
+  pmt::State state1 = sensor->Read();
+  std::this_thread::sleep_for(std::chrono::milliseconds(kSleepMs));
+  pmt::State state2 = sensor->Read();
+
+  // Verify returned power
+  const float watt = pmt::PMT::watts(state1, state2);
+  CHECK_THAT(watt, Catch::Matchers::WithinAbs(kWatt, 1.f));
+
+  // Verify power calculation from energy
+  const float joules = pmt::PMT::joules(state1, state2);
+  const float seconds = pmt::PMT::seconds(state1, state2);
+  REQUIRE(seconds > 0);
+  CHECK_THAT(joules / seconds, Catch::Matchers::WithinAbs(kWatt, 1.f));
+}
+
+/**
+ * Verify the Dummy sensor reporting energy
+ * - The Dummy sensor is configured to report a fixed energy value
+ *
+ * Test validates that:
+ * 1. Sensor correctly reports configured power value
+ * 2. Power calculation from energy matches configured value
+ */
+TEST_CASE("Set joules") {
+  const float kWatt = 42;
+  const size_t kSleepMs = GENERATE(1, 1e2, 1e3);
+
+  // Initialize
+  auto sensor = pmt::Dummy::Create();
+  sensor->UseFixedJoules({kWatt});
+
+  // Take two measurements
+  pmt::State state1 = sensor->Read();
+  std::this_thread::sleep_for(std::chrono::milliseconds(kSleepMs));
+  pmt::State state2 = sensor->Read();
+
+  // Verify returned power
+  const float watt = pmt::PMT::watts(state1, state2);
+  CHECK_THAT(watt, Catch::Matchers::WithinAbs(kWatt, 1.f));
+
+  // Verify power calculation from energy
+  const float joules = pmt::PMT::joules(state1, state2);
+  const float seconds = pmt::PMT::seconds(state1, state2);
+  REQUIRE(seconds > 0);
+  CHECK_THAT(joules / seconds, Catch::Matchers::WithinAbs(kWatt, 1.f));
+}
+
+/**
+ * Verify the Dummy sensor reporting power
+ * - The Dummy sensor is configured to report a fixed power value
+ *
+ * Test validates that:
+ * 1. Sensor correctly reports sum of configured power values
+ * 2. Power calculation from energy matches sum of configured values
+ * 3. Individual sensor values are correctly reported
+ */
+TEST_CASE("Set watt for 2 sensors") {
+  const std::vector<float> kWatt{{42, 420}};
+  const float kWattSum = std::accumulate(kWatt.begin(), kWatt.end(), 0.f);
+  const size_t kSleepMs = GENERATE(1, 1e2, 1e3);
+
+  // Initialize
+  auto sensor = pmt::Dummy::Create();
+  sensor->UseFixedWatts(kWatt);
+
+  // Take two measurements
+  pmt::State state1 = sensor->Read();
+  std::this_thread::sleep_for(std::chrono::milliseconds(kSleepMs));
+  pmt::State state2 = sensor->Read();
+
+  // Verify returned power
+  const float watt = pmt::PMT::watts(state1, state2);
+  CHECK_THAT(watt, Catch::Matchers::WithinAbs(kWattSum, 1.f));
+
+  // Verify power calculation from energy
+  const float joules = pmt::PMT::joules(state1, state2);
+  const float seconds = pmt::PMT::seconds(state1, state2);
+  REQUIRE(seconds > 0);
+  CHECK_THAT(joules / seconds, Catch::Matchers::WithinAbs(kWattSum, 1.f));
+
+  // Verify individual sensor values
+  for (size_t i = 0; i < kWatt.size(); ++i) {
+    CHECK_THAT(state1.watts(i + 1), Catch::Matchers::WithinAbs(kWatt[i], 1.f));
+    CHECK_THAT(state2.watts(i + 1), Catch::Matchers::WithinAbs(kWatt[i], 1.f));
+  }
+}
\ No newline at end of file
diff --git a/xilinx/Xilinx.cpp b/xilinx/Xilinx.cpp
index 56426006a3e7e6973215b2fb312f0a2cbca71b29..508c01654139d26bda43208ff59aa9b865fad848 100644
--- a/xilinx/Xilinx.cpp
+++ b/xilinx/Xilinx.cpp
@@ -1,6 +1,7 @@
 #include "Xilinx.h"
 
 #include <istream>
+#include <sstream>
 #include <stdexcept>
 #include <vector>
 
@@ -8,13 +9,17 @@
 #include <ext/alloc_traits.h>
 #include <stdlib.h>
 
-namespace anonymous {
+#include "common/Exception.h"
+
+namespace {
 float GetPower(std::string &filename) {
   // Open power file, e.g.
   // /sys/devices/pci0000:a0/0000:a0:03.1/0000:a1:00.0/hwmon/hwmon3/power1_input
   std::ifstream file(filename, std::ios::in | std::ios::binary);
   if (errno != 0) {
-    throw std::runtime_error("Could not open: " + filename);
+    std::ostringstream message;
+    message << "Could not open: " << filename;
+    throw pmt::Exception(message.str().c_str());
   }
 
   // This file has one line with instantenous power consumption in uW
@@ -23,43 +28,23 @@ float GetPower(std::string &filename) {
   return power;
 }
 
-}  // namespace anonymous
+}  // namespace
 namespace pmt::xilinx {
 
-class XilinxState {
- public:
-  operator State();
-  Timestamp timestamp_;
-  double watt_ = 0;
-  double joules_ = 0;
-};
-
 class XilinxImpl : public Xilinx {
  public:
   XilinxImpl(const char *device);
 
  private:
-  State GetState() override { return GetXilinxState(); }
+  State GetState() override;
 
   virtual const char *GetDumpFilename() override {
     return "/tmp/pmt_xilinx.out";
   }
 
   std::string filename_;
-
-  XilinxState previous_state_;
-  XilinxState GetXilinxState();
 };
 
-XilinxState::operator State() {
-  State state;
-  state.timestamp_ = timestamp_;
-  state.name_[0] = "device";
-  state.joules_[0] = joules_;
-  state.watt_[0] = watt_;
-  return state;
-}
-
 std::unique_ptr<Xilinx> Xilinx::Create(const char *device) {
   return std::make_unique<XilinxImpl>(device);
 }
@@ -67,20 +52,13 @@ std::unique_ptr<Xilinx> Xilinx::Create(const char *device) {
 XilinxImpl::XilinxImpl(const char *device) {
   char *pmt_device = getenv("PMT_DEVICE");
   filename_ = pmt_device ? pmt_device : device;
-
-  previous_state_ = GetXilinxState();
-  previous_state_.joules_ = 0;
 }
 
-XilinxState XilinxImpl::GetXilinxState() {
-  XilinxState state;
+State XilinxImpl::GetState() {
+  State state;
   state.timestamp_ = GetTime();
-  state.watt_ = anonymous::GetPower(filename_) * 1e-6;
-  state.joules_ = previous_state_.joules_;
-  const float watts = (state.watt_ + previous_state_.watt_) / 2;
-  const double duration = seconds(previous_state_.timestamp_, state.timestamp_);
-  state.joules_ += watts * duration;
-  previous_state_ = state;
+  state.name_[0] = "device";
+  state.watt_[0] = ::GetPower(filename_) * 1e-6;
   return state;
 }