From 30ce13620bda44295da5c38806020c8652f7367d Mon Sep 17 00:00:00 2001
From: Jan David Mol <mol@astron.nl>
Date: Wed, 4 Sep 2024 13:17:38 +0000
Subject: [PATCH] Resolve L2SS-1955 & L2SS-1902 "Remove psoc"

---
 .editorconfig                                 |   3 +
 .gitlab-ci.yml                                |  10 +-
 CDB/stations/DTS.json                         |   1 -
 CDB/stations/common.json                      |  48 +-
 CDB/stations/cs.json                          |   1 -
 CDB/stations/rs.json                          |   1 -
 CDB/stations/simulators_ConfigDb.json         |  13 -
 CDB/stations/testenv_cs001.json               |  13 -
 CDB/stations/testenv_rs307.json               | 875 ++++++++++++++++++
 README.md                                     |  12 +-
 .../startup/02-devices.py                     |   1 -
 infra/dev/all.hcl                             |   4 +
 infra/dev/services.hcl                        |   4 +
 infra/dev/services/variables.hcl              |   4 +
 infra/dev/tango.hcl                           |   4 +
 infra/dev/tango/tango.hcl                     |  18 +-
 infra/dev/tango/variables.hcl                 |   4 +
 infra/env/common.yaml                         |   1 -
 infra/jobs/station/Makefile                   |   7 +-
 sbin/prepare_dev_env.sh                       |   1 +
 sbin/run_integration_test.sh                  |  32 +-
 .../docs/source/devices/overview.rst          |   8 -
 .../docs/source/devices/psoc.rst              |   6 -
 tangostationcontrol/docs/source/index.rst     |   1 -
 .../integration_test/common/__init__.py       |   0
 .../common/base_device_classes/__init__.py    |   0
 .../power_hierarchy_tests.py                  | 378 ++++++++
 .../common/device_beamlet_tests.py            | 136 +++
 .../common/device_bst_tests.py                |  27 +
 .../common/device_digitalbeam_tests.py        | 252 +++++
 .../common/device_hba_tests.py                | 410 ++++++++
 .../device_observation_control_tests.py       | 308 ++++++
 .../common/device_sdp_tests.py                |  18 +
 .../common/device_sdpfirmware_tests.py        |  28 +
 .../common/device_sst_tests.py                |  73 ++
 .../common/device_tilebeam_tests.py           | 280 ++++++
 .../common/device_xst_tests.py                |  18 +
 .../configDB/LOFAR_ConfigDb.json              |  25 -
 .../configDB/simulators_ConfigDb.json         |  13 -
 .../devices/antennafield/test_device_hba.py   | 396 +-------
 .../test_power_hierarchy.py                   | 357 +------
 .../default/devices/test_device_beamlet.py    | 125 +--
 .../default/devices/test_device_bst.py        |  19 +-
 .../devices/test_device_digitalbeam.py        | 243 +----
 .../default/devices/test_device_metadata.py   |   1 -
 .../test_device_observation_control.py        | 337 +------
 .../devices/test_device_observation_field.py  |   4 +-
 .../default/devices/test_device_psoc.py       |  11 -
 .../default/devices/test_device_sdp.py        |  28 +-
 .../devices/test_device_sdpfirmware.py        |  32 +-
 .../default/devices/test_device_sst.py        |  65 +-
 .../default/devices/test_device_tilebeam.py   | 264 +-----
 .../default/devices/test_device_unb2.py       |   2 +-
 .../default/devices/test_device_xst.py        |  20 +-
 .../devices/test_observation_client.py        |   6 +-
 .../remote_station/__init__.py                |   0
 .../remote_station/antennafield/__init__.py   |   2 +
 .../antennafield/test_device_hba.py           |  20 +
 .../base_device_classes/__init__.py           |   0
 .../test_power_hierarchy.py                   |  21 +
 .../remote_station/test_device_beamlet.py     |  11 +
 .../remote_station/test_device_bst.py         |  13 +
 .../remote_station/test_device_digitalbeam.py |  23 +
 .../test_device_observation_control.py        |  37 +
 .../remote_station/test_device_sdp.py         |  16 +
 .../remote_station/test_device_sdpfirmware.py |  15 +
 .../remote_station/test_device_sst.py         |  13 +
 .../remote_station/test_device_tilebeam.py    |  40 +
 .../remote_station/test_device_xst.py         |  13 +
 .../clients/snmp/attribute_classes.py         |  80 --
 .../tangostationcontrol/common/constants.py   |   3 +
 .../tangostationcontrol/devices/__init__.py   |   2 -
 .../base_device_classes/power_hierarchy.py    |   6 -
 .../tangostationcontrol/devices/psoc.py       | 180 ----
 .../tangostationcontrol/devices/types.py      |   1 -
 .../test/dummy_observation_settings.py        |  12 +-
 .../test/clients/test_snmp_client.py          |  37 -
 .../test_hierarchy_device.py                  |  10 +-
 .../test/devices/test_psoc_device.py          |  30 -
 .../test/observation/test_observation.py      |  22 +-
 .../test_observation_controller.py            |   8 +-
 .../observation/test_observation_field.py     |  16 +-
 82 files changed, 3318 insertions(+), 2261 deletions(-)
 create mode 100644 CDB/stations/testenv_rs307.json
 delete mode 100644 tangostationcontrol/docs/source/devices/psoc.rst
 create mode 100644 tangostationcontrol/integration_test/common/__init__.py
 create mode 100644 tangostationcontrol/integration_test/common/base_device_classes/__init__.py
 create mode 100644 tangostationcontrol/integration_test/common/base_device_classes/power_hierarchy_tests.py
 create mode 100644 tangostationcontrol/integration_test/common/device_beamlet_tests.py
 create mode 100644 tangostationcontrol/integration_test/common/device_bst_tests.py
 create mode 100644 tangostationcontrol/integration_test/common/device_digitalbeam_tests.py
 create mode 100644 tangostationcontrol/integration_test/common/device_hba_tests.py
 create mode 100644 tangostationcontrol/integration_test/common/device_observation_control_tests.py
 create mode 100644 tangostationcontrol/integration_test/common/device_sdp_tests.py
 create mode 100644 tangostationcontrol/integration_test/common/device_sdpfirmware_tests.py
 create mode 100644 tangostationcontrol/integration_test/common/device_sst_tests.py
 create mode 100644 tangostationcontrol/integration_test/common/device_tilebeam_tests.py
 create mode 100644 tangostationcontrol/integration_test/common/device_xst_tests.py
 delete mode 100644 tangostationcontrol/integration_test/default/devices/test_device_psoc.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/__init__.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/antennafield/__init__.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/antennafield/test_device_hba.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/base_device_classes/__init__.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/base_device_classes/test_power_hierarchy.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/test_device_beamlet.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/test_device_bst.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/test_device_digitalbeam.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/test_device_observation_control.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/test_device_sdp.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/test_device_sdpfirmware.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/test_device_sst.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/test_device_tilebeam.py
 create mode 100644 tangostationcontrol/integration_test/remote_station/test_device_xst.py
 delete mode 100644 tangostationcontrol/tangostationcontrol/devices/psoc.py
 delete mode 100644 tangostationcontrol/test/devices/test_psoc_device.py

diff --git a/.editorconfig b/.editorconfig
index df0525fcb..6c2122894 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -17,6 +17,9 @@ ij_smart_tabs = false
 ij_visual_guides = none
 ij_wrap_on_typing = false
 
+[Makefile]
+indent_style = tab
+
 [{*.bash,*.sh,*.zsh}]
 indent_size = 2
 tab_width = 2
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5e397f0af..457fd4669 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -305,11 +305,17 @@ unit_test:
       - log/
       - .jumppad/logs/
 
-integration_test_docker:
+integration_test_core:
   extends: .test_docker
   script:
     #    Do not remove 'bash' or statement will be ignored by primitive docker shell
-    - bash -e $CI_PROJECT_DIR/sbin/run_integration_test.sh --no-build --save-logs --module="tango"
+    - bash -e $CI_PROJECT_DIR/sbin/run_integration_test.sh --no-build --save-logs --module="tango" --station=cs
+
+integration_test_remote:
+  extends: .test_docker
+  script:
+    #    Do not remove 'bash' or statement will be ignored by primitive docker shell
+    - bash -e $CI_PROJECT_DIR/sbin/run_integration_test.sh --no-build --save-logs --module="tango" --station=rs
 
 service_test_docker:
   extends: .test_docker
diff --git a/CDB/stations/DTS.json b/CDB/stations/DTS.json
index cc1753666..ccaa2b931 100644
--- a/CDB/stations/DTS.json
+++ b/CDB/stations/DTS.json
@@ -18,7 +18,6 @@
               "Control_Children": [
                 "STAT/EC/1",
                 "STAT/CCD/1",
-                "STAT/PSOC/1",
                 "STAT/PCON/1",
                 "STAT/Configuration/1",
                 "STAT/Calibration/1",
diff --git a/CDB/stations/common.json b/CDB/stations/common.json
index 4096102dc..5f9ad7a71 100644
--- a/CDB/stations/common.json
+++ b/CDB/stations/common.json
@@ -11,11 +11,6 @@
         "Power_Available_In_State": ["HIBERNATE"]
       }
     },
-    "PSOC": {
-      "properties": {
-        "Power_Available_In_State": ["HIBERNATE"]
-      }
-    },
     "PCON": {
       "properties": {
         "Power_Available_In_State": ["HIBERNATE"]
@@ -118,7 +113,7 @@
             "properties": {
               "Power_Children": [
                 "STAT/PCON/1",
-                "STAT/PSOC/1"
+                "STAT/CCD/1"
               ],
               "OPC_Server_Name": [
                 "10.87.2.126"
@@ -193,47 +188,6 @@
         }
       }
     },
-    "PSOC": {
-      "STAT": {
-        "PSOC": {
-          "STAT/PSOC/1": {
-            "properties": {
-              "Power_Children": [
-                "STAT/CCD/1"
-              ],
-              "SNMP_host": [
-                "10.99.250.80"
-              ],
-              "SNMP_community": [
-                "public"
-              ],
-              "SNMP_mib_dir": [
-                "devices/mibs/PowerNet-MIB.mib"
-              ],
-              "SNMP_timeout": [
-                "10.0"
-              ],
-              "SNMP_version": [
-                "1"
-              ],
-              "SNMP_use_simulators": [
-                "False"
-              ],
-              "PSOC_sockets": [
-                "socket_1",
-                "socket_2",
-                "socket_3",
-                "socket_4",
-                "socket_5",
-                "socket_6",
-                "socket_7",
-                "socket_8"
-              ]
-            }
-          }
-        }
-      }
-    },
     "PCON": {
       "STAT": {
         "PCON": {
diff --git a/CDB/stations/cs.json b/CDB/stations/cs.json
index 536f242eb..7b0f161f2 100644
--- a/CDB/stations/cs.json
+++ b/CDB/stations/cs.json
@@ -18,7 +18,6 @@
               "Control_Children": [
                 "STAT/EC/1",
                 "STAT/CCD/1",
-                "STAT/PSOC/1",
                 "STAT/PCON/1",
                 "STAT/Configuration/1",
                 "STAT/Calibration/1",
diff --git a/CDB/stations/rs.json b/CDB/stations/rs.json
index a7f52c16e..50ee06212 100644
--- a/CDB/stations/rs.json
+++ b/CDB/stations/rs.json
@@ -17,7 +17,6 @@
               "Control_Children": [
                 "STAT/EC/1",
                 "STAT/CCD/1",
-                "STAT/PSOC/1",
                 "STAT/PCON/1",
                 "STAT/Configuration/1",
                 "STAT/Calibration/1",
diff --git a/CDB/stations/simulators_ConfigDb.json b/CDB/stations/simulators_ConfigDb.json
index 910139622..38c0a04c6 100644
--- a/CDB/stations/simulators_ConfigDb.json
+++ b/CDB/stations/simulators_ConfigDb.json
@@ -171,19 +171,6 @@
                 }
             }
         },
-        "PSOC": {
-            "STAT": {
-                "PSOC": {
-                    "STAT/PSOC/1": {
-                        "properties": {
-                            "SNMP_use_simulators": [
-                                "True"
-                            ]
-                        }
-                    }
-                }
-            }
-        },
         "RECVH": {
             "STAT": {
                 "RECVH": {
diff --git a/CDB/stations/testenv_cs001.json b/CDB/stations/testenv_cs001.json
index c670652b6..46909da40 100644
--- a/CDB/stations/testenv_cs001.json
+++ b/CDB/stations/testenv_cs001.json
@@ -365,19 +365,6 @@
                 }
             }
         },
-        "PSOC": {
-            "STAT": {
-                "PSOC": {
-                    "STAT/PSOC/1": {
-                        "properties": {
-                            "SNMP_use_simulators": [
-                                "True"
-                            ]
-                        }
-                    }
-                }
-            }
-        },
         "RECVH": {
             "STAT": {
                 "RECVH": {
diff --git a/CDB/stations/testenv_rs307.json b/CDB/stations/testenv_rs307.json
new file mode 100644
index 000000000..ede3458be
--- /dev/null
+++ b/CDB/stations/testenv_rs307.json
@@ -0,0 +1,875 @@
+{
+    "servers": {
+        "APSCT": {
+            "STAT": {
+                "APSCT": {
+                    "STAT/APSCT/L0": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "apsct-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "APSCT_On_Off_timeout": [
+                                "1"
+                            ]
+                        }
+                    },
+                    "STAT/APSCT/L1": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "apsct-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "APSCT_On_Off_timeout": [
+                                "1"
+                            ]
+                        }
+                    },
+                    "STAT/APSCT/H0": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "apsct-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "APSCT_On_Off_timeout": [
+                                "1"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "CCD": {
+            "STAT": {
+                "CCD": {
+                    "STAT/CCD/1": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "ccd-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "CCD_On_Off_timeout": [
+                                "1"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "EC": {
+            "STAT": {
+                "EC": {
+                    "STAT/EC/1": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "ec-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "OPC_Node_Path_Prefix": [
+                              "3:ServerInterfaces",
+                              "4:Environmental_Control"
+                            ],
+                            "OPC_namespace": [
+                              "http://Environmental_Control"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "APSPU": {
+            "STAT": {
+                "APSPU": {
+                    "STAT/APSPU/L0": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "apspu-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ]
+                        }
+                    },
+                    "STAT/APSPU/L1": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "apspu-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ]
+                        }
+                    },
+                    "STAT/APSPU/H0": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "apspu-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "Beamlet": {
+            "STAT": {
+                "Beamlet": {
+                    "STAT/Beamlet/LBA": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "FPGA_beamlet_output_hdr_eth_destination_mac_RW_default": [
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB"
+                            ],
+                            "FPGA_beamlet_output_hdr_ip_destination_address_RW_default": [
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1"
+                            ]
+                        }
+                    },
+                    "STAT/Beamlet/HBA": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "FPGA_beamlet_output_hdr_eth_destination_mac_RW_default": [
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB"
+                            ],
+                            "FPGA_beamlet_output_hdr_ip_destination_address_RW_default": [
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "DigitalBeam": {
+            "STAT": {
+                "DigitalBeam": {
+                    "STAT/DigitalBeam/LBA": {
+                        "properties": {
+                            "Beam_tracking_interval": [
+                                "1.0"
+                            ],
+                            "Beam_tracking_preparation_period": [
+                                "0.5"
+                            ]
+                        }
+                    },
+                    "STAT/DigitalBeam/HBA": {
+                        "properties": {
+                            "Beam_tracking_interval": [
+                                "1.0"
+                            ],
+                            "Beam_tracking_preparation_period": [
+                                "0.5"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "TemperatureManager": {
+            "STAT": {
+                "TemperatureManager": {
+                    "STAT/TemperatureManager/1": {
+                        "properties": {
+                            "Alarm_Error_List": [
+                                "APSCT, APSCT_TEMP_error_R",
+                                "APSPU, APSPU_TEMP_error_R",
+                                "UNB2, UNB2_TEMP_error_R",
+                                "RECVH, RECV_TEMP_error_R",
+                                "RECVL, RECV_TEMP_error_R"
+                            ],
+                            "Shutdown_Device_List": [
+                                "STAT/SDPFirmware/LBA",
+                                "STAT/SDPFirmware/HBA",
+                                "STAT/SDP/LBA",
+                                "STAT/SDP/HBA",
+                                "STAT/UNB2/L0",
+                                "STAT/UNB2/L1",
+                                "STAT/UNB2/H0",
+                                "STAT/RECVH/H0",
+                                "STAT/RECVL/L0",
+                                "STAT/RECVL/L1",
+                                "STAT/APSCT/L0",
+                                "STAT/APSCT/L1",
+                                "STAT/APSCT/H0",
+                                "STAT/CCD/1",
+                                "STAT/APSPU/L0",
+                                "STAT/APSPU/L1",
+                                "STAT/APSPU/H0"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "PCON": {
+            "STAT": {
+                "PCON": {
+                    "STAT/PCON/1": {
+                        "properties": {
+                            "SNMP_use_simulators": [
+                                "True"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "PSOC": {
+            "STAT": {
+                "PSOC": {
+                    "STAT/PSOC/1": {
+                        "properties": {
+                            "SNMP_use_simulators": [
+                                "True"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "RECVH": {
+            "STAT": {
+                "RECVH": {
+                    "STAT/RECVH/H0": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "recvh-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "RCU_On_Off_timeout": [
+                                "1"
+                            ],
+                            "RCU_DTH_On_Off_timeout": [
+                                "1"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "RECVL": {
+            "STAT": {
+                "RECVL": {
+                    "STAT/RECVL/L0": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "recvl-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "RCU_On_Off_timeout": [
+                                "1"
+                            ],
+                            "RCU_DTH_On_Off_timeout": [
+                                "1"
+                            ]
+                        }
+                    },
+                    "STAT/RECVL/L1": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "recvl-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "RCU_On_Off_timeout": [
+                                "1"
+                            ],
+                            "RCU_DTH_On_Off_timeout": [
+                                "1"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "SDPFirmware": {
+            "STAT": {
+                "SDPFirmware": {
+                    "STAT/SDPFirmware/LBA": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "Firmware_Boot_timeout": [
+                                "1.0"
+                            ]
+                        }
+                    },
+                    "STAT/SDPFirmware/HBA": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "Firmware_Boot_timeout": [
+                                "1.0"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "SDP": {
+            "STAT": {
+                "SDP": {
+                    "STAT/SDP/LBA": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ]
+                        }
+                    },
+                    "STAT/SDP/HBA": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "BST": {
+            "STAT": {
+                "BST": {
+                    "STAT/BST/LBA": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "FPGA_bst_offload_hdr_eth_destination_mac_RW_default": [
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB"
+                            ],
+                            "FPGA_bst_offload_hdr_ip_destination_address_RW_default": [
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1"
+                            ]
+                        }
+                    },
+                    "STAT/BST/HBA": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "FPGA_bst_offload_hdr_eth_destination_mac_RW_default": [
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB"
+                            ],
+                            "FPGA_bst_offload_hdr_ip_destination_address_RW_default": [
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "SST": {
+            "STAT": {
+                "SST": {
+                    "STAT/SST/LBA": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "FPGA_sst_offload_hdr_eth_destination_mac_RW_default": [
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB"
+                            ],
+                            "FPGA_sst_offload_hdr_ip_destination_address_RW_default": [
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1"
+                            ]
+                        }
+                    },
+                    "STAT/SST/HBA": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "FPGA_sst_offload_hdr_eth_destination_mac_RW_default": [
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB"
+                            ],
+                            "FPGA_sst_offload_hdr_ip_destination_address_RW_default": [
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "XST": {
+            "STAT": {
+                "XST": {
+                    "STAT/XST/LBA": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "FPGA_xst_offload_hdr_eth_destination_mac_RW_default": [
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB"
+                            ],
+                            "FPGA_xst_offload_hdr_ip_destination_address_RW_default": [
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1"
+                            ]
+                        }
+                    },
+                    "STAT/XST/HBA": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "sdptr-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "FPGA_xst_offload_hdr_eth_destination_mac_RW_default": [
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB",
+                                "01:23:45:67:89:AB"
+                            ],
+                            "FPGA_xst_offload_hdr_ip_destination_address_RW_default": [
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1",
+                                "127.0.0.1"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "UNB2": {
+            "STAT": {
+                "UNB2": {
+                    "STAT/UNB2/L0": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "unb2-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "UNB2_On_Off_timeout": [
+                                "1"
+                            ]
+                        }
+                    },
+                    "STAT/UNB2/L1": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "unb2-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "UNB2_On_Off_timeout": [
+                                "1"
+                            ]
+                        }
+                    },
+                    "STAT/UNB2/H0": {
+                        "properties": {
+                            "OPC_Server_Name": [
+                                "unb2-sim.service.consul"
+                            ],
+                            "OPC_Server_Port": [
+                                "4840"
+                            ],
+                            "OPC_Time_Out": [
+                                "5.0"
+                            ],
+                            "UNB2_On_Off_timeout": [
+                                "1"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "TileBeam": {
+            "STAT": {
+                "TileBeam": {
+                    "STAT/TileBeam/HBA": {
+                        "properties": {
+                            "Tracking_enabled_RW_default": [ "True" ],
+                            "Beam_tracking_interval": [
+                                "10.0"
+                            ]
+                        }
+                    }
+                }
+            }
+        },
+        "StationManager": {
+            "STAT": {
+              "StationManager": {
+                "STAT/StationManager/1": {
+                  "properties": {
+                    "Suppress_State_Transition_Failures": [
+                        "True"
+                    ]
+                  }
+                }
+              }
+            }
+        }
+    }
+}
diff --git a/README.md b/README.md
index fedae2f2b..9e77facda 100644
--- a/README.md
+++ b/README.md
@@ -114,17 +114,6 @@ If you think the network is lingering as an error you can use
 
 ### Inspect services
 
-Now we can start all containers, and make sure everything is up:
-
-```sh
-make start
-make status
-```
-
-If not, you can inspect why with `docker logs <container>`. The containers will
-automatically be restarted on reboot or failure. Stop them explicitly to bring
-them down (`make stop <container>`).
-
 Most notably, you will have web interfaces available at:
 
 - http://localhost:8888 (Jupyter Lab)
@@ -164,6 +153,7 @@ Next change the version in the following places:
 
 * 0.42.2 Add protection control device shutting down station during over temperature
          Use the station manager `protection_lock_RW` to see if the station is locked against further damage.
+         Add integration tests for remote stations
 * 0.42.1 Added lock around commands in AsyncDevices to prevent concurrent execution
 * 0.42.0 Change CS032 port mappings to prevent beamlet overlap
 * 0.41.1 Reduce log size for value changes and metadata publications drastically
diff --git a/docker/jupyter-lab/ipython-profiles/stationcontrol-jupyter/startup/02-devices.py b/docker/jupyter-lab/ipython-profiles/stationcontrol-jupyter/startup/02-devices.py
index 5de897e4b..42831e92a 100644
--- a/docker/jupyter-lab/ipython-profiles/stationcontrol-jupyter/startup/02-devices.py
+++ b/docker/jupyter-lab/ipython-profiles/stationcontrol-jupyter/startup/02-devices.py
@@ -80,7 +80,6 @@ stationmanager = OptionalDeviceProxy("STAT/StationManager/1")
 ccd = OptionalDeviceProxy("STAT/CCD/1")
 ec = OptionalDeviceProxy("STAT/EC/1")
 pcon = OptionalDeviceProxy("STAT/PCON/1")
-psoc = OptionalDeviceProxy("STAT/PSOC/1")
 protectioncontrol = OptionalDeviceProxy("STAT/ProtectionControl/1")
 configuration = OptionalDeviceProxy("STAT/Configuration/1")
 calibration = OptionalDeviceProxy("STAT/Calibration/1")
diff --git a/infra/dev/all.hcl b/infra/dev/all.hcl
index 0d0a81ea6..67f8717f3 100644
--- a/infra/dev/all.hcl
+++ b/infra/dev/all.hcl
@@ -12,6 +12,10 @@ variable "debug_host" {
   default = ""
 }
 
+variable "station_type" {
+  default = "cs"
+}
+
 module "nomad" {
   source    = "./nomad"
   variables = {
diff --git a/infra/dev/services.hcl b/infra/dev/services.hcl
index 3ee8d999f..16049f539 100644
--- a/infra/dev/services.hcl
+++ b/infra/dev/services.hcl
@@ -5,6 +5,10 @@ variable "image_tag" {
   default = "latest"
 }
 
+variable "station_type" {
+  default = "cs"
+}
+
 module "nomad" {
   source    = "./nomad"
   variables = {
diff --git a/infra/dev/services/variables.hcl b/infra/dev/services/variables.hcl
index ebc49b1ac..6dbadd784 100644
--- a/infra/dev/services/variables.hcl
+++ b/infra/dev/services/variables.hcl
@@ -2,6 +2,10 @@ variable "nomad_cluster" {
   default = ""
 }
 
+variable "station_type" {
+  default = "cs"
+}
+
 variable "lofar20_dir" {
   default = ""
 }
diff --git a/infra/dev/tango.hcl b/infra/dev/tango.hcl
index 8c77d9aad..4cddf0006 100644
--- a/infra/dev/tango.hcl
+++ b/infra/dev/tango.hcl
@@ -12,6 +12,10 @@ variable "debug_host" {
   default = ""
 }
 
+variable "station_type" {
+  default = "cs"
+}
+
 module "nomad" {
   source    = "./nomad"
   variables = {
diff --git a/infra/dev/tango/tango.hcl b/infra/dev/tango/tango.hcl
index 6e8aada4e..976ee8090 100644
--- a/infra/dev/tango/tango.hcl
+++ b/infra/dev/tango/tango.hcl
@@ -87,6 +87,7 @@ resource "exec" "dsconfig" {
     DOCKER_HOST=docker_host()
     TANGO_HOST="tango.service.consul:10000"
     TAG=variable.image_tag
+    STATION_TYPE=variable.station_type
   }
   working_directory = "${variable.lofar20_dir}"
   timeout = "300s"
@@ -108,10 +109,19 @@ resource "exec" "dsconfig" {
   bash sbin/dsconfig.sh --update CDB/stations/l1.json
   bash sbin/dsconfig.sh --update CDB/stations/lba.json
   bash sbin/dsconfig.sh --update CDB/stations/h0.json
-  bash sbin/dsconfig.sh --update CDB/stations/hba_core.json
-  bash sbin/dsconfig.sh --update CDB/stations/cs.json
-  bash sbin/dsconfig.sh --update CDB/stations/cs001.json
-  bash sbin/dsconfig.sh --update CDB/stations/testenv_cs001.json
+  if [ "$STATION_TYPE" = "rs" ]; then
+    echo "Configuring dsconfig for remote station"
+    bash sbin/dsconfig.sh --update CDB/stations/hba_remote.json
+    bash sbin/dsconfig.sh --update CDB/stations/rs.json
+    bash sbin/dsconfig.sh --update CDB/stations/rs307.json
+    bash sbin/dsconfig.sh --update CDB/stations/testenv_rs307.json
+  else
+    echo "Configuring dsconfig for core station"
+    bash sbin/dsconfig.sh --update CDB/stations/hba_core.json
+    bash sbin/dsconfig.sh --update CDB/stations/cs.json
+    bash sbin/dsconfig.sh --update CDB/stations/cs001.json
+    bash sbin/dsconfig.sh --update CDB/stations/testenv_cs001.json
+  fi
   EOF
 }
 
diff --git a/infra/dev/tango/variables.hcl b/infra/dev/tango/variables.hcl
index f396640ec..cacc9d706 100644
--- a/infra/dev/tango/variables.hcl
+++ b/infra/dev/tango/variables.hcl
@@ -2,6 +2,10 @@ variable "nomad_cluster" {
   default = ""
 }
 
+variable "station_type" {
+  default = "cs"
+}
+
 variable "image_tag" {
   default = "latest"
 }
diff --git a/infra/env/common.yaml b/infra/env/common.yaml
index b45c930f8..93fe34c00 100644
--- a/infra/env/common.yaml
+++ b/infra/env/common.yaml
@@ -73,7 +73,6 @@ devices:
   - ObservationControl
   - Metadata
   - PCON
-  - PSOC
   - RECVH
   - RECVL
   - SDP
diff --git a/infra/jobs/station/Makefile b/infra/jobs/station/Makefile
index 9520f70ab..d564746ac 100644
--- a/infra/jobs/station/Makefile
+++ b/infra/jobs/station/Makefile
@@ -1,10 +1,15 @@
 TAG ?= latest
 STATION ?= dev
+STATION_TYPE ?= cs
 DIR_OUT ?= .
 DIR_SRC += .
 SRC_JOBS += $(wildcard $(addsuffix /*.levant.nomad, $(DIR_SRC)))
 JOBS := $(patsubst %.levant.nomad,%.nomad, $(SRC_JOBS))
-ENV ?= ../../env/common.yaml ../../env/cs.yaml
+ifeq ($(STATION_TYPE), cs)
+		ENV ?= ../../env/common.yaml ../../env/cs.yaml
+else
+		ENV ?= ../../env/common.yaml ../../env/rs.yaml
+endif
 
 .PHONY: render
 
diff --git a/sbin/prepare_dev_env.sh b/sbin/prepare_dev_env.sh
index 3fa5149d3..1889f1926 100755
--- a/sbin/prepare_dev_env.sh
+++ b/sbin/prepare_dev_env.sh
@@ -37,6 +37,7 @@ if ! [ -x "$(command -v levant)" ]; then
   chmod +x ./.bin/levant
 fi
 
+echo "Generating jobs for station configuration: $STATION_TYPE"
 make -C infra/jobs/station DIR_OUT="$( realpath "infra/dev/jobs/station")" render
 
 docker_volume="dev_nomad_station"
diff --git a/sbin/run_integration_test.sh b/sbin/run_integration_test.sh
index 67a57bc73..140aa91f3 100755
--- a/sbin/run_integration_test.sh
+++ b/sbin/run_integration_test.sh
@@ -36,12 +36,15 @@ function usage {
     echo ""
     echo "./$(basename "$0") --module=<tango|services|all>
       Only start given subset of the infrastructure, defaults to all"
+    echo ""
+    echo "./$(basename "$0") --station=<cs|rs>
+      Configure testing for core or remote station"
 }
 
 
 
 # list of arguments expected in the input
-optstring_long="help,no-build,skip-tests,preserve,save-logs,interactive,module::"
+optstring_long="help,no-build,skip-tests,preserve,save-logs,interactive,module::,station::"
 optstring="h"
 
 options=$(getopt -l ${optstring_long} -o ${optstring} -- "$@")
@@ -49,6 +52,7 @@ options=$(getopt -l ${optstring_long} -o ${optstring} -- "$@")
 eval set -- "$options"
 
 module="all"
+station="cs"
 
 while true; do
   case ${1} in
@@ -83,6 +87,10 @@ while true; do
       shift
       module="$1"
       ;;
+    --station)
+      shift
+      station="$1"
+      ;;
     --)
     shift
     break;;
@@ -90,6 +98,8 @@ while true; do
   shift
 done
 
+export STATION_TYPE=$station
+
 if [ "${module}" == "services" ]; then
   echo "module=services, enabling skip-tests and preservation"
   export no_tests=1
@@ -286,6 +296,7 @@ jumppad_options=(  # these don't seem to propagate
   --var="lofar20_dir=$LOFAR20_DIR"
   --var="image_tag=$TAG"
   --var="debug_host=$LOCAL_IP"
+  --var="station_type=$STATION_TYPE"
 )
 
 echo "Start module: $module"
@@ -316,7 +327,7 @@ echo "Using tango host $TANGO_HOST"
 
 # Devices list is used to explitly word split when supplied to commands, must
 # disable shellcheck SC2086 for each case.
-DEVICES=(device-stationmanager device-aps device-apsct device-ccd device-ec device-apspu device-sdpfirmware device-sdp device-recvh device-recvl device-bst device-sst device-unb2 device-xst device-beamlet device-digitalbeam device-tilebeam device-psoc device-pcon device-afh device-afl device-protectioncontrol device-observationcontrol device-configuration device-calibration device-metadata)
+DEVICES=(device-stationmanager device-aps device-apsct device-ccd device-ec device-apspu device-sdpfirmware device-sdp device-recvh device-recvl device-bst device-sst device-unb2 device-xst device-beamlet device-digitalbeam device-tilebeam device-psoc device-afh device-afl device-protectioncontrol device-observationcontrol device-configuration device-calibration device-metadata)
 
 # Wait for devices to restart
 
@@ -328,10 +339,13 @@ fi
 
 # Start the integration test
 
-integration_test default
-
-integration_test tilebeam_performance "device-sdpfirmware device-sdp device-recvh device-recvl device-tilebeam device-afh device-afl" "${LOFAR20_DIR}/CDB/integrations/tilebeam_cluster_ConfigDb.json"
-
-integration_test digitalbeam_performance "device-sdpfirmware device-sdp device-recvh device-recvl device-digitalbeam device-beamlet device-afh device-afl" "${LOFAR20_DIR}/CDB/integrations/digitalbeam_cluster_ConfigDb.json"
-
-integration_test configuration "device-configuration"
+if [ "$STATION_TYPE" = "cs" ]; then
+  echo "Running integration tests for core station"
+  integration_test default
+  integration_test tilebeam_performance "device-sdpfirmware device-sdp device-recvh device-recvl device-tilebeam device-afh device-afl" "${LOFAR20_DIR}/CDB/integrations/tilebeam_cluster_ConfigDb.json"
+  integration_test digitalbeam_performance "device-sdpfirmware device-sdp device-recvh device-recvl device-digitalbeam device-beamlet device-afh device-afl" "${LOFAR20_DIR}/CDB/integrations/digitalbeam_cluster_ConfigDb.json"
+  integration_test configuration "device-configuration"
+else
+  echo "Running integration tests for remote station"
+  integration_test remote_station
+fi
diff --git a/tangostationcontrol/docs/source/devices/overview.rst b/tangostationcontrol/docs/source/devices/overview.rst
index a1bbe0d9b..c5c2bfdda 100644
--- a/tangostationcontrol/docs/source/devices/overview.rst
+++ b/tangostationcontrol/docs/source/devices/overview.rst
@@ -33,8 +33,6 @@ This package implements the *Station Control (SC)* part of a LOFAR2.0 station, t
         APSCT;
         APSPU;
         UNB2;
-
-        PSOC;
       }
 
       subgraph clusterSDP {
@@ -70,11 +68,6 @@ This package implements the *Station Control (SC)* part of a LOFAR2.0 station, t
         APSPUTR;
       }
       APSPU -> APSPUTR;
-
-      subgraph clusterPSOC {
-        label = "PSOC";
-      }
-      PSOC -> clusterPSOC;
     }
 
 A brief description of each of these devices:
@@ -94,7 +87,6 @@ Auxilliary devices that control hardware are:
 * `APSCT` device controls the ASPCT clock selection and distribution board,
 * `APSPU` device controls the APSPU 48V distribution board,
 * `UNB2` device controls the Uniboards that hold the SDP FPGAs (and thus firmware).
-* `PSOC` device controls the power sockets (230V distribution).
 
 Finally, the stack holds the auxilliary devices that control the software devices. They connect to too many devices to draw:
 
diff --git a/tangostationcontrol/docs/source/devices/psoc.rst b/tangostationcontrol/docs/source/devices/psoc.rst
deleted file mode 100644
index c3265005f..000000000
--- a/tangostationcontrol/docs/source/devices/psoc.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-.. _psoc:
-
-PSOC
---------------------
-
-The ``psoc == DeviceProxy("STAT/PSOC/1")`` device controls the Power Distribution Unit (PSOC).
diff --git a/tangostationcontrol/docs/source/index.rst b/tangostationcontrol/docs/source/index.rst
index 872a3b4ca..44bb396b5 100644
--- a/tangostationcontrol/docs/source/index.rst
+++ b/tangostationcontrol/docs/source/index.rst
@@ -29,7 +29,6 @@ Even without having access to any LOFAR2.0 hardware, you can install the full st
    devices/bst-sst-xst
    devices/station-manager
    devices/docker
-   devices/psoc
    devices/ccd
    devices/ec
    devices/configuration
diff --git a/tangostationcontrol/integration_test/common/__init__.py b/tangostationcontrol/integration_test/common/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tangostationcontrol/integration_test/common/base_device_classes/__init__.py b/tangostationcontrol/integration_test/common/base_device_classes/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tangostationcontrol/integration_test/common/base_device_classes/power_hierarchy_tests.py b/tangostationcontrol/integration_test/common/base_device_classes/power_hierarchy_tests.py
new file mode 100644
index 000000000..aded09f1f
--- /dev/null
+++ b/tangostationcontrol/integration_test/common/base_device_classes/power_hierarchy_tests.py
@@ -0,0 +1,378 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Power Hierarchy module integration test
+"""
+import logging
+
+from tango import DevState, DeviceProxy
+
+from integration_test import base
+from integration_test.device_proxy import TestDeviceProxy
+from lofar_station_client.common import CaseInsensitiveString
+from tangostationcontrol.common.constants import N_rcu, N_rcu_inp
+from tangostationcontrol.devices.base_device_classes.hierarchy_device import (
+    NotFoundException,
+    HierarchyMatchingFilter,
+)
+from tangostationcontrol.devices.base_device_classes.power_hierarchy import (
+    PowerHierarchyControlDevice,
+)
+
+logger = logging.getLogger()
+
+
+class DevicePowerHierarchyControlTests(base.IntegrationTestCase):
+    """Integration Test class for PowerHierarchyDevice methods"""
+
+    __test__ = False
+
+    pwr_attr_name = "hardware_powered_R"
+
+    stationmanager_name = "STAT/StationManager/1"
+    ec_name = "STAT/EC/1"
+    aps_l0_name = "STAT/APS/L0"
+    aps_l1_name = "STAT/APS/L1"
+    aps_h0_name = "STAT/APS/H0"
+    apsct_name = "STAT/APSCT/H0"
+    apspu_h0_name = "STAT/APSPU/H0"
+    apspu_l0_name = "STAT/APSPU/L0"
+    apspu_l1_name = "STAT/APSPU/L1"
+    ccd_name = "STAT/CCD/1"
+    pcon_name = "STAT/PCON/1"
+    sdp_name = "STAT/SDP/HBA0"
+    unb2_h0_name = "STAT/UNB2/H0"
+    unb2_l0_name = "STAT/UNB2/L0"
+    recvh_name = "STAT/RECVH/H0"
+    recvl_name = "STAT/RECVL/L0"
+
+    def setUp(
+        self,
+        station_name: str = "CS001",
+        antennafield_name: str = None,
+        sdp_name: str = None,
+        sdpfirmware_name: str = None,
+    ):
+        super().setUp()
+
+        if antennafield_name is None or sdp_name is None or sdpfirmware_name is None:
+            raise RuntimeError("Missing required device names to initialize tests")
+
+        self.station_name = station_name
+
+        self.antennafield_name = antennafield_name
+        self.sdp_name = sdp_name
+        self.sdpfirmware_name = sdpfirmware_name
+
+        self.setup_all_devices()
+
+    def _unacceptable_exceptions(self):
+        """Return the set of exceptions raised by the last state transition
+        in the StationManager, that we do not accept.
+
+        We must accept exceptions since we do not emulate interaction with
+        actual hardware. In this function, we make sure to just ignore those
+        which we know will be raised even in a sunny-day scenario."""
+
+        result = []
+
+        for ex_str in self.stationmanager_proxy.last_requested_transition_exceptions_R:
+            # Skip "vetted" exceptions that involve switching power
+            # on actual hardware, which obviously won't power on in
+            # our simulators.
+            if "Failed to execute command_inout on device" in ex_str:
+                if "command power_hardware_on" in ex_str:
+                    continue
+                if "command power_hardware_off" in ex_str:
+                    continue
+
+            # Anything left is not acceptable
+            result.append(ex_str)
+
+        return result
+
+    def setup_stationmanager_proxy(self):
+        """Initialise StationManager device"""
+        stationmanager_proxy = TestDeviceProxy(self.stationmanager_name)
+        # extend timeout for running commands, as state transitions can take a long time
+        stationmanager_proxy.set_timeout_millis(60000)
+
+        stationmanager_proxy.off()
+        stationmanager_proxy.initialise()
+        stationmanager_proxy.on()
+        self.assertEqual(stationmanager_proxy.state(), DevState.ON)
+        return stationmanager_proxy
+
+    def setup_proxy_off(self, device_name: str):
+        """Initialise proxy and turn off device"""
+        proxy = TestDeviceProxy(device_name)
+        proxy.off()
+        return proxy
+
+    def setup_all_devices(self):
+        """Initialise all Tango devices needed for state transitions"""
+        self.stationmanager_proxy = self.setup_stationmanager_proxy()
+
+        self.ec_proxy = self.setup_proxy_off(self.ec_name)
+        self.aps_l0_proxy = self.setup_proxy_off(self.aps_l0_name)
+        self.pcon_proxy = self.setup_proxy_off(self.pcon_name)
+        self.ccd_proxy = self.setup_proxy_off(self.ccd_name)
+        self.apspu_h0_proxy = self.setup_proxy_off(self.apspu_h0_name)
+        self.apspu_l0_proxy = self.setup_proxy_off(self.apspu_l0_name)
+        self.apsct_proxy = self.setup_proxy_off(self.apsct_name)
+        self.unb2_h0_proxy = self.setup_proxy_off(self.unb2_h0_name)
+        self.unb2_l0_proxy = self.setup_proxy_off(self.unb2_l0_name)
+        self.recvh_proxy = self.setup_proxy_off(self.recvh_name)
+        self.recvl_proxy = self.setup_proxy_off(self.recvl_name)
+        self.sdpfirmware_proxy = self.setup_proxy_off(self.sdpfirmware_name)
+        self.sdp_proxy = self.setup_proxy_off(self.sdp_name)
+        self.antennafield_proxy = self.setup_proxy_off(self.antennafield_name)
+
+    def test_power_sequence_definition(self):
+        """
+        Test whether Power Sequence is correctly retrieved from the HierarchyDevice
+        """
+        self.setup_stationmanager_proxy()
+        self.setup_all_devices()
+
+        stationmanager_ph = PowerHierarchyControlDevice()
+        stationmanager_ph.init(self.stationmanager_name)
+        children_hierarchy = stationmanager_ph.children(depth=2)
+
+        # Check if EC is child of StationManager
+        ec_name = self.ec_name.casefold()
+        self.assertTrue(ec_name in children_hierarchy)
+        self.assertTrue(isinstance(children_hierarchy[ec_name]["proxy"], DeviceProxy))
+
+        # Check if CCD is child of EC
+        self.assertTrue(
+            self.ccd_name.casefold() in children_hierarchy[ec_name]["children"].keys()
+        )
+
+        # Check if EC retrieves correctly its parent state (StationManager -> ON)
+        ec_ph = PowerHierarchyControlDevice()
+        ec_ph.init(self.ec_name)
+        self.assertEqual(ec_ph.parent_state(), DevState.ON)
+        # Check if child reads correctly a parent attribute
+        self.assertEqual(
+            ec_ph.read_parent_attribute("Station_Name_R"),
+            self.station_name,
+        )
+
+    def test_off_to_hibernate(self):
+        """Test Tango devices are correctly triggered in the OFF to HIBERNATE transition"""
+        self.assertEqual(self.pcon_proxy.state(), DevState.OFF)
+        self.assertEqual(self.ccd_proxy.state(), DevState.OFF)
+        # Switch from OFF to HIBERNATE
+        self.stationmanager_proxy.station_hibernate()
+        self.assertEqual(self.stationmanager_proxy.station_state_R.name, "HIBERNATE")
+        self.assertEqual(
+            self.stationmanager_proxy.requested_station_state_R.name, "HIBERNATE"
+        )
+        self.assertEqual(self.pcon_proxy.state(), DevState.ON)
+        self.assertEqual(self.ccd_proxy.state(), DevState.ON)
+
+        logger.info(
+            "Exceptions suppressed in test_off_to_hibernate: %s",
+            self.stationmanager_proxy.last_requested_transition_exceptions_R,
+        )
+
+        self.assertListEqual([], self._unacceptable_exceptions())
+
+    def test_hibernate_to_standby(self):
+        """
+        Test whether Tango devices are correctly triggered in the HIBERNATE to STANDBY transition
+        """
+        # Switch from OFF to HIBERNATE
+        self.stationmanager_proxy.station_hibernate()
+        self.assertEqual(self.stationmanager_proxy.station_state_R.name, "HIBERNATE")
+        self.assertEqual(
+            self.stationmanager_proxy.requested_station_state_R.name, "HIBERNATE"
+        )
+        self.assertEqual(self.apspu_h0_proxy.state(), DevState.OFF)
+        self.assertEqual(self.apspu_l0_proxy.state(), DevState.OFF)
+        self.assertEqual(self.apsct_proxy.state(), DevState.OFF)
+        self.assertEqual(self.unb2_h0_proxy.state(), DevState.OFF)
+        self.assertEqual(self.unb2_l0_proxy.state(), DevState.OFF)
+        self.assertEqual(self.recvh_proxy.state(), DevState.OFF)
+        self.assertEqual(self.recvl_proxy.state(), DevState.OFF)
+        self.assertEqual(self.sdpfirmware_proxy.state(), DevState.OFF)
+        # Switch from HIBERNATE to STANDBY
+        self.stationmanager_proxy.station_standby()
+        self.assertEqual(self.stationmanager_proxy.station_state_R.name, "STANDBY")
+        self.assertEqual(
+            self.stationmanager_proxy.requested_station_state_R.name, "STANDBY"
+        )
+        self.assertEqual(self.apspu_h0_proxy.state(), DevState.ON)
+        self.assertEqual(self.apspu_l0_proxy.state(), DevState.ON)
+        self.assertEqual(self.apsct_proxy.state(), DevState.ON)
+        self.assertEqual(self.unb2_h0_proxy.state(), DevState.ON)
+        self.assertEqual(self.unb2_l0_proxy.state(), DevState.ON)
+        self.assertEqual(self.recvh_proxy.state(), DevState.ON)
+        self.assertEqual(self.recvl_proxy.state(), DevState.ON)
+        self.assertEqual(self.sdpfirmware_proxy.state(), DevState.ON)
+        # Check if SDP Firmware is booted with factory image
+        firmware_images = self.sdpfirmware_proxy.FPGA_boot_image_RW.tolist()
+        self.assertListEqual(
+            [0] * len(firmware_images),
+            firmware_images,
+        )
+
+        logger.info(
+            "Exceptions suppressed: %s",
+            self.stationmanager_proxy.last_requested_transition_exceptions_R,
+        )
+
+        self.assertListEqual([], self._unacceptable_exceptions())
+
+    def test_standby_to_on(self):
+        """
+        Test whether Tango devices are correctly triggered in the STANDBY to ON transition
+        """
+        # Switch from OFF to HIBERNATE
+        self.stationmanager_proxy.station_hibernate()
+        # Switch from HIBERNATE to STANDBY
+        self.stationmanager_proxy.station_standby()
+        # Switch from STANDBY to ON
+        self.assertEqual(self.sdp_proxy.state(), DevState.OFF)
+        self.assertEqual(self.antennafield_proxy.state(), DevState.OFF)
+        self.stationmanager_proxy.station_on()
+        self.assertEqual(self.stationmanager_proxy.station_state_R.name, "ON")
+        self.assertEqual(self.stationmanager_proxy.requested_station_state_R.name, "ON")
+        self.assertEqual(self.sdp_proxy.state(), DevState.ON)
+        self.assertEqual(self.antennafield_proxy.state(), DevState.ON)
+
+        # Test if DTH and DAB are disabled
+        self.assertListEqual(
+            self.recvh_proxy.RCU_DTH_on_R.tolist(),
+            [[False] * N_rcu_inp] * N_rcu,
+        )
+        self.assertListEqual(
+            self.antennafield_proxy.RCU_DAB_filter_on_RW.tolist(),
+            [False] * self.antennafield_proxy.nr_antennas_R,
+        )
+
+        logger.info(
+            "Exceptions suppressed: %s",
+            self.stationmanager_proxy.last_requested_transition_exceptions_R,
+        )
+
+        self.assertListEqual([], self._unacceptable_exceptions())
+
+    def test_on_to_standby(self):
+        """
+        Test whether Tango devices are correctly triggered in the ON to STANDBY transition
+        """
+        # Switch from OFF to HIBERNATE
+        self.stationmanager_proxy.station_hibernate()
+        # Switch from HIBERNATE to STANDBY
+        self.stationmanager_proxy.station_standby()
+        # Switch from STANDBY to ON
+        self.stationmanager_proxy.station_on()
+        # Reverse to STANDBY
+        self.stationmanager_proxy.station_standby()
+        self.assertEqual(self.stationmanager_proxy.station_state_R.name, "STANDBY")
+        self.assertEqual(
+            self.stationmanager_proxy.requested_station_state_R.name, "STANDBY"
+        )
+        self.assertEqual(self.sdp_proxy.state(), DevState.OFF)
+        self.assertEqual(self.antennafield_proxy.state(), DevState.OFF)
+
+        logger.info(
+            "Exceptions suppressed: %s",
+            self.stationmanager_proxy.last_requested_transition_exceptions_R,
+        )
+
+        self.assertListEqual([], self._unacceptable_exceptions())
+
+    def test_standby_to_hibernate(self):
+        """
+        Test whether Tango devices are correctly triggered in the STANDBY to HIBERNATE transition
+        """
+        # Switch from OFF to HIBERNATE
+        self.stationmanager_proxy.station_hibernate()
+        # Switch from HIBERNATE to STANDBY
+        self.stationmanager_proxy.station_standby()
+        # Reverse to HIBERNATE
+        self.stationmanager_proxy.station_hibernate()
+        self.assertEqual(self.stationmanager_proxy.station_state_R.name, "HIBERNATE")
+        self.assertEqual(
+            self.stationmanager_proxy.requested_station_state_R.name, "HIBERNATE"
+        )
+        self.assertEqual(self.apspu_h0_proxy.state(), DevState.OFF)
+        self.assertEqual(self.apspu_l0_proxy.state(), DevState.OFF)
+        self.assertEqual(self.apsct_proxy.state(), DevState.OFF)
+        self.assertEqual(self.unb2_h0_proxy.state(), DevState.OFF)
+        self.assertEqual(self.unb2_l0_proxy.state(), DevState.OFF)
+        self.assertEqual(self.recvh_proxy.state(), DevState.OFF)
+        self.assertEqual(self.recvl_proxy.state(), DevState.OFF)
+        self.assertEqual(self.sdpfirmware_proxy.state(), DevState.OFF)
+
+        logger.info(
+            "Exceptions suppressed: %s",
+            self.stationmanager_proxy.last_requested_transition_exceptions_R,
+        )
+
+        self.assertListEqual([], self._unacceptable_exceptions())
+
+    def test_branch_child_deep_tree(self):
+        """
+        Test whether Tango devices are correctly retrieved with branch_child function
+        """
+        self.setup_all_devices()
+        # Create a Hierarchy Device from Device STAT/RECVH/H0
+        recvh_ph = PowerHierarchyControlDevice()
+        recvh_ph.init(self.recvh_name)
+        self.assertEqual(recvh_ph.parent(), "stat/apspu/h0")
+        # Branch child method must not return its direct parent
+        self.assertRaises(
+            NotFoundException,
+            recvh_ph.branch_child,
+            "*/apspu/h*",
+            HierarchyMatchingFilter.REGEX,
+        )
+        # Test if returns another device with the right query
+        self.assertEqual(
+            recvh_ph.branch_child("*/apspu/*", HierarchyMatchingFilter.REGEX).name(),
+            CaseInsensitiveString(self.apspu_l0_name),
+        )
+        # Test if returns a device in the tree with depth > 1
+        self.assertEqual(
+            recvh_ph.branch_child("*/aps/l*", HierarchyMatchingFilter.REGEX).name(),
+            CaseInsensitiveString(self.aps_l0_name),
+        )
+
+    def test_branch_children_names_deep_tree(self):
+        """
+        Test whether Tango devices are correctly retrieved with
+        branch_children_names function
+        """
+        self.setup_all_devices()
+        # Create a Hierarchy Device from Device STAT/RECVH/H0
+        recvh_ph = PowerHierarchyControlDevice()
+        recvh_ph.init(self.recvh_name)
+        self.assertEqual(recvh_ph.parent(), "stat/apspu/h0")
+        self.assertEqual(recvh_ph.children(), {})
+        # Branch_children_names method must not return its direct parent
+        self.assertListEqual(
+            recvh_ph.branch_children_names("*/apspu/h*", HierarchyMatchingFilter.REGEX),
+            [],
+        )
+        # Test if returns another device with the right query
+        self.assertListEqual(
+            recvh_ph.branch_children_names("*/apspu/*", HierarchyMatchingFilter.REGEX),
+            [
+                CaseInsensitiveString(self.apspu_l0_name),
+                CaseInsensitiveString(self.apspu_l1_name),
+            ],
+        )
+        # Test if returns a device in the tree with depth > 1
+        self.assertListEqual(
+            recvh_ph.branch_children_names("*/aps/*", HierarchyMatchingFilter.REGEX),
+            [
+                CaseInsensitiveString(self.aps_l0_name),
+                CaseInsensitiveString(self.aps_l1_name),
+                CaseInsensitiveString(self.aps_h0_name),
+            ],
+        )
diff --git a/tangostationcontrol/integration_test/common/device_beamlet_tests.py b/tangostationcontrol/integration_test/common/device_beamlet_tests.py
new file mode 100644
index 000000000..e4f05f623
--- /dev/null
+++ b/tangostationcontrol/integration_test/common/device_beamlet_tests.py
@@ -0,0 +1,136 @@
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+import time
+from ctypes import c_short
+
+import numpy
+import numpy.testing
+
+from integration_test.default.devices.base import TestDeviceBase
+
+from tango import DevState
+from tangostationcontrol.common.constants import (
+    N_beamlets_ctrl,
+    S_pn,
+    CLK_200_MHZ,
+    CLK_160_MHZ,
+    N_pn,
+    A_pn,
+)
+
+
+class BeamletDeviceTests(TestDeviceBase):
+    """Integration test class for device Beamlet"""
+
+    __test__ = False
+
+    def setUp(
+        self,
+        beamlet_name: str = None,
+        sdp_name: str = None,
+        sdpfirmware_name: str = None,
+    ):
+        """Intentionally recreate the device object in each test"""
+
+        if beamlet_name is None or sdp_name is None or sdpfirmware_name is None:
+            raise RuntimeError("Missing required device names to initialize tests")
+
+        super().setUp(beamlet_name)
+
+        self.sdp_proxy = self.setup_proxy(sdp_name, defaults=True)
+        self.sdp_proxy.nyquist_zone_RW = [[2] * S_pn] * N_pn
+
+        self.sdpfirmware_proxy = self.setup_proxy(sdpfirmware_name, defaults=True)
+        self.sdpfirmware_proxy.clock_RW = CLK_200_MHZ
+
+    def test_pointing_to_zenith(self):
+        self.proxy.initialise()
+        self.proxy.on()
+
+        # The subband frequency of HBA subband 0 is 200 MHz,
+        # so its period is 5 ns. A delay of 2.5e-9 seconds is thus half a period,
+        # and result in a 180 degree phase offset.
+        delays = numpy.array([[[2.5e-9] * N_pn] * A_pn] * N_beamlets_ctrl)
+
+        calculated_bf_weights = self.proxy.calculate_bf_weights(delays.flatten())
+
+        # With a unit weight of 2**14, we thus expect beamformer weights of -2**14 + 0j,
+        # which is 49152 when read as an uint32.
+        self.assertEqual(-(2**14), c_short(49152).value)  # check our calculations
+        expected_bf_weights = numpy.array(
+            [49152] * N_pn * A_pn * N_beamlets_ctrl, dtype=numpy.uint32
+        )
+
+        numpy.testing.assert_almost_equal(expected_bf_weights, calculated_bf_weights)
+
+    def test_subband_select_change(self):
+        # Change subband
+        self.proxy.off()
+        self.proxy.initialise()
+        self.assertEqual(DevState.STANDBY, self.proxy.state())
+        self.proxy.subband_select_RW = [10] * N_beamlets_ctrl
+        self.proxy.on()
+        self.assertEqual(DevState.ON, self.proxy.state())
+
+        # The subband frequency of HBA subband 10 is 201953125 Hz
+        # so its period is 4.95 ns ca, half period is 2.4758e-9
+        delays = numpy.array([[[2.4758e-9] * N_pn] * A_pn] * N_beamlets_ctrl)
+        calculated_bf_weights_subband_10 = self.proxy.calculate_bf_weights(
+            delays.flatten()
+        )
+
+        self.assertEqual(-(2**14), c_short(49152).value)  # check our calculations
+        expected_bf_weights_10 = numpy.array(
+            [49152] * N_pn * A_pn * N_beamlets_ctrl, dtype=numpy.uint32
+        )
+        numpy.testing.assert_almost_equal(
+            expected_bf_weights_10, calculated_bf_weights_subband_10
+        )
+
+    def test_sdp_clock_change(self):
+        sdpfirmware_proxy = self.sdpfirmware_proxy
+        self.proxy.initialise()
+        self.proxy.subband_select_RW = numpy.array(
+            list(range(317)) + [316] + list(range(318, N_beamlets_ctrl)),
+            dtype=numpy.uint32,
+        )
+        self.proxy.on()
+
+        # any non-zero delay should result in different weights for different clocks
+        delays = numpy.array([[[2.5e-9] * N_pn] * A_pn] * N_beamlets_ctrl)
+
+        sdpfirmware_proxy.clock_RW = CLK_200_MHZ
+        time.sleep(3)  # wait for beamlet device to process change event
+        calculated_bf_weights_200 = self.proxy.calculate_bf_weights(delays.flatten())
+
+        sdpfirmware_proxy.clock_RW = CLK_160_MHZ
+        time.sleep(3)  # wait for beamlet device to process change event
+        calculated_bf_weights_160 = self.proxy.calculate_bf_weights(delays.flatten())
+
+        sdpfirmware_proxy.clock_RW = CLK_200_MHZ
+        time.sleep(3)  # wait for beamlet device to process change event
+        calculated_bf_weights_200_v2 = self.proxy.calculate_bf_weights(delays.flatten())
+
+        # outcome should be changed back and forth across clock changes
+        self.assertTrue((calculated_bf_weights_200 != calculated_bf_weights_160).all())
+        self.assertTrue(
+            (calculated_bf_weights_200 == calculated_bf_weights_200_v2).all()
+        )
+
+        # change subbands
+        self.proxy.off()
+        self.proxy.initialise()
+        self.proxy.subband_select_RW = [317] * N_beamlets_ctrl
+        self.proxy.on()
+        calculated_bf_weights_200_v3 = self.proxy.calculate_bf_weights(delays.flatten())
+        self.assertTrue(
+            (calculated_bf_weights_200_v2 != calculated_bf_weights_200_v3).all()
+        )
+
+        sdpfirmware_proxy.clock_RW = CLK_160_MHZ
+        time.sleep(1)  # wait for beamlet device to process change event
+        calculated_bf_weights_160_v2 = self.proxy.calculate_bf_weights(delays.flatten())
+        self.assertTrue(
+            (calculated_bf_weights_160 != calculated_bf_weights_160_v2).all()
+        )
diff --git a/tangostationcontrol/integration_test/common/device_bst_tests.py b/tangostationcontrol/integration_test/common/device_bst_tests.py
new file mode 100644
index 000000000..ac2b01e1f
--- /dev/null
+++ b/tangostationcontrol/integration_test/common/device_bst_tests.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+from integration_test.default.devices.base import TestDeviceBase
+
+
+class BSTDeviceTests(TestDeviceBase):
+    __test__ = False
+
+    def setUp(
+        self, bst_name: str = None, sdpfirmware_name: str = None, sdp_name: str = None
+    ):
+        if bst_name is None or sdp_name is None or sdpfirmware_name is None:
+            raise RuntimeError("Missing required device names to initialize tests")
+
+        self.sdpfirmware_name = sdpfirmware_name
+        self.sdp_name = sdp_name
+
+        """Intentionally recreate the device object in each test"""
+        super().setUp(bst_name)
+
+    def test_device_read_all_attributes(self):
+        # We need to connect to SDP first to read some of our attributes
+        self.setup_proxy(self.sdpfirmware_name, defaults=True)
+        self.setup_proxy(self.sdp_name, defaults=True)
+
+        super().test_device_read_all_attributes()
diff --git a/tangostationcontrol/integration_test/common/device_digitalbeam_tests.py b/tangostationcontrol/integration_test/common/device_digitalbeam_tests.py
new file mode 100644
index 000000000..4ec817c05
--- /dev/null
+++ b/tangostationcontrol/integration_test/common/device_digitalbeam_tests.py
@@ -0,0 +1,252 @@
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
+import logging
+import time
+
+import numpy
+import timeout_decorator
+
+from integration_test.device_proxy import TestDeviceProxy
+from integration_test.default.devices.base import TestDeviceBase
+from tangostationcontrol.common.constants import (
+    MAX_ANTENNA,
+    N_beamlets_ctrl,
+    N_pn,
+    A_pn,
+    CLK_200_MHZ,
+    CLK_160_MHZ,
+    CS001_TILES,
+)
+from tangostationcontrol.devices.base_device_classes.antennafield_device import (
+    AntennaStatus,
+    AntennaUse,
+)
+
+logger = logging.getLogger()
+
+# tests will break in weird ways if our constants change beyond these constraints
+assert CS001_TILES >= 2
+assert MAX_ANTENNA >= 2
+
+
+class DigitalBeamDeviceTests(TestDeviceBase):
+    __test__ = False
+
+    antenna_status_ok = numpy.array([AntennaStatus.OK] * MAX_ANTENNA)
+    antenna_status_only_second = numpy.array(
+        [AntennaStatus.BROKEN]
+        + [AntennaStatus.OK]
+        + [AntennaStatus.BROKEN] * (MAX_ANTENNA - 2)
+    )
+    antenna_use_ok = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA)
+
+    def setUp(
+        self,
+        tile_number: int = CS001_TILES,
+        digitalbeam_name: str = None,
+        antennafield_name: str = None,
+        beamlet_name: str = None,
+        sdp_name: str = None,
+        sdpfirmware_name: str = None,
+        recv_name: str = None,
+    ):
+        """Intentionally recreate the device object in each test"""
+
+        if (
+            digitalbeam_name is None
+            or antennafield_name is None
+            or beamlet_name is None
+            or sdp_name is None
+            or sdpfirmware_name is None
+            or recv_name is None
+        ):
+            raise RuntimeError("Missing required device names to initialize tests")
+
+        super().setUp(digitalbeam_name)
+
+        self.tile_number = tile_number
+
+        self.recv_proxy = self.setup_proxy(recv_name, defaults=True)
+        self.sdpfirmware_proxy = self.setup_proxy(sdpfirmware_name, defaults=True)
+        self.sdp_proxy = self.setup_proxy(sdp_name)
+        self.beamlet_proxy = self.initialise_beamlet_proxy(beamlet_name)
+
+        control_mapping = [[1, i] for i in range(tile_number)]
+        sdp_mapping = [[i // 6, i % 6] for i in range(tile_number)]
+        self.antennafield_proxy = self.setup_proxy(
+            antennafield_name,
+            cb=lambda x: {
+                x.put_property(
+                    {
+                        "Control_to_RECV_mapping": numpy.array(
+                            control_mapping
+                        ).flatten(),
+                        "Antenna_to_SDP_Mapping": numpy.array(sdp_mapping).flatten(),
+                        "Antenna_Status": self.antenna_status_ok,
+                        "Antenna_Use": self.antenna_use_ok,
+                        "Antenna_Cables": ["50m", "80m"] * (tile_number // 2),
+                        "Antenna_Sets": ["FIRST", "ALL"],
+                        "Antenna_Set_Masks": [
+                            "1" + ("0" * (tile_number - 1)),
+                            "1" * tile_number,
+                        ],
+                        "Antenna_Type": "HBA",
+                    }
+                )
+            },
+        )
+
+        self.addCleanup(TestDeviceProxy.test_device_turn_off, beamlet_name)
+
+    def initialise_beamlet_proxy(self, name: str = None):
+        if not name and self.beamlet_proxy:
+            beamlet_proxy = self.beamlet_proxy
+        else:
+            beamlet_proxy = TestDeviceProxy(name)
+
+        beamlet_proxy.off()
+        beamlet_proxy.initialise()
+        return beamlet_proxy
+
+    def test_pointing_to_zenith_clock_change(self):
+        self.beamlet_proxy.on()
+
+        # Set first (default) clock configuration
+        self.sdpfirmware_proxy.clock_RW = CLK_200_MHZ
+        time.sleep(1)
+
+        self.proxy.initialise()
+        self.proxy.Tracking_enabled_RW = False
+        self.proxy.on()
+
+        # Point to Zenith
+        self.proxy.set_pointing(
+            numpy.array(
+                [["AZELGEO", "0rad", "1.570796rad"]] * self.proxy.nr_beamlets_R
+            ).flatten()
+        )
+
+        # beam weights should now be non-zero, we don't actually check their values for correctness
+        FPGA_bf_weights_pp_clock200 = self.beamlet_proxy.FPGA_bf_weights_pp_RW.flatten()
+        self.assertNotEqual(0, sum(FPGA_bf_weights_pp_clock200))
+
+        self.beamlet_proxy = self.initialise_beamlet_proxy()
+        self.beamlet_proxy.on()
+
+        # Change clock configuration
+        self.sdpfirmware_proxy.clock_RW = CLK_160_MHZ
+        time.sleep(1)
+
+        FPGA_bf_weights_pp_clock160 = self.beamlet_proxy.FPGA_bf_weights_pp_RW.flatten()
+        # Assert some values are different
+        self.assertNotEqual(
+            sum(FPGA_bf_weights_pp_clock160), sum(FPGA_bf_weights_pp_clock200)
+        )
+
+    def test_pointing_to_zenith_subband_change(self):
+        self.beamlet_proxy.subband_select_RW = numpy.array(
+            list(range(317)) + [316] + list(range(318, N_beamlets_ctrl)),
+            dtype=numpy.uint32,
+        )
+        self.beamlet_proxy.on()
+
+        self.proxy.initialise()
+        self.proxy.Tracking_enabled_RW = False
+        self.proxy.on()
+
+        # Point to Zenith
+        self.proxy.set_pointing(
+            numpy.array(
+                [["AZELGEO", "0rad", "1.570796rad"]] * self.proxy.nr_beamlets_R
+            ).flatten()
+        )
+        # Store values with first subband configuration
+        FPGA_bf_weights_pp_subband_v1 = (
+            self.beamlet_proxy.FPGA_bf_weights_pp_RW.flatten()
+        )
+
+        # Restart beamlet proxy
+        self.beamlet_proxy = self.initialise_beamlet_proxy()
+        self.beamlet_proxy.subband_select_RW = [317] * N_beamlets_ctrl
+        self.beamlet_proxy.on()
+
+        # Store values with second subband configuration
+        FPGA_bf_weights_pp_subband_v2 = (
+            self.beamlet_proxy.FPGA_bf_weights_pp_RW.flatten()
+        )
+        # Assert some values are different
+        self.assertNotEqual(
+            sum(FPGA_bf_weights_pp_subband_v1), sum(FPGA_bf_weights_pp_subband_v2)
+        )
+
+    def test_set_pointing_masked_enable(self):
+        """Verify that only selected inputs are written"""
+
+        self.proxy.initialise()
+        self.proxy.Tracking_enabled_RW = False
+        self.proxy.on()
+
+        # Enable first input
+        self.proxy.Antenna_Set_RW = "FIRST"
+
+        # fill weights with values the beamformer will never use (|x| > 1)
+        impossible_values = numpy.array([[2] * N_beamlets_ctrl * A_pn] * N_pn)
+        self.beamlet_proxy.FPGA_bf_weights_pp_RW = impossible_values
+
+        self.proxy.set_pointing(
+            numpy.array(
+                [["AZELGEO", "0rad", "1.570796rad"]] * self.proxy.nr_beamlets_R
+            ).flatten()
+        )
+
+        # Verify all impossible values are replaced with other values for all inputs
+        # which should be non-zero for the first antenna, and zero for the rest
+        FPGA_bf_weights_pp_RW = self.beamlet_proxy.FPGA_bf_weights_pp_RW.reshape(
+            (N_pn * A_pn, -1)
+        )
+
+        # first antenna should have values from the beamformer, so not 0 and not 2
+        self.assertTrue(
+            numpy.all(numpy.not_equal(0, FPGA_bf_weights_pp_RW[0, :])),
+            f"{FPGA_bf_weights_pp_RW}",
+        )
+        self.assertTrue(
+            numpy.all(numpy.not_equal(2, FPGA_bf_weights_pp_RW[0, :])),
+            f"{FPGA_bf_weights_pp_RW}",
+        )
+
+        # rest of the antennas should have been given a weight of 0, as they
+        # were not in the usage mask.
+        self.assertTrue(
+            numpy.all(numpy.equal(0, FPGA_bf_weights_pp_RW[1 : self.tile_number, :])),
+            f"{FPGA_bf_weights_pp_RW}",
+        )
+
+    @timeout_decorator.timeout(15)
+    def test_beam_tracking_90_percent_interval(self):
+        """Verify that the beam tracking operates within 95% of interval"""
+
+        self.proxy.initialise()
+        self.proxy.Tracking_enabled_RW = True
+        self.proxy.on()
+
+        interval = float(
+            self.proxy.get_property("Beam_tracking_interval")["Beam_tracking_interval"][
+                0
+            ]
+        )
+
+        # Allow beam tracking time to settle
+        time.sleep(interval * 3)
+
+        # We have to poll at regular interval due to not working subscribe
+        # events
+        for _ in range(0, 5):
+            error = self.proxy.Pointing_error_R[0]
+            self.assertTrue(
+                -interval * 0.10 < error < interval * 0.10,
+                f"Error: {error} larger than {interval * 0.10}",
+            )
+            logger.info("BeamTracking error: %s", error)
+            time.sleep(interval)
diff --git a/tangostationcontrol/integration_test/common/device_hba_tests.py b/tangostationcontrol/integration_test/common/device_hba_tests.py
new file mode 100644
index 000000000..6a77499c2
--- /dev/null
+++ b/tangostationcontrol/integration_test/common/device_hba_tests.py
@@ -0,0 +1,410 @@
+#  Copyright (C)  2024 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
+import logging
+import time
+import numpy
+
+from integration_test.default.devices.base import TestDeviceBase
+from tango import DevState
+
+from tangostationcontrol.common.constants import (
+    N_elements,
+    MAX_ANTENNA,
+    N_pol,
+    N_rcu,
+    N_rcu_inp,
+    CS001_TILES,
+    CLK_160_MHZ,
+    N_pn,
+    S_pn,
+)
+from tangostationcontrol.common.frequency_bands import bands
+from tangostationcontrol.devices.base_device_classes.antennafield_device import (
+    AntennaStatus,
+    AntennaUse,
+)
+
+# tests will break in weird ways if our constants change beyond these constraints
+assert CS001_TILES >= 2
+assert N_pn * S_pn >= 4
+assert N_rcu_inp * N_rcu >= 2
+
+logger = logging.getLogger()
+
+
+class HBADeviceTests(TestDeviceBase):
+    """Integration base test class for device HBA"""
+
+    __test__ = False
+
+    def setUp(
+        self,
+        tiles: int = CS001_TILES,
+        afh_name: str = None,
+        sdpfirmware_name: str = None,
+        sdp_name: str = None,
+    ):
+        if afh_name is None or sdp_name is None or sdpfirmware_name is None:
+            raise RuntimeError("Missing required device names to initialize tests")
+
+        self.tiles = tiles
+
+        self.stationmanager_proxy = self.setup_proxy("STAT/StationManager/1")
+
+        # Setup will dump current properties and restore them for us
+        super().setUp(afh_name)
+
+        # Typical tests emulate 'CS001_TILES' number of antennas in
+        # the AntennaField. 'MAX_ANTENNA' is the number of inputs
+        # offered by a backing RECV device. Each antenna has
+        # N_pol (2) inputs.
+        self.proxy.put_property(
+            {
+                "Power_to_RECV_mapping": numpy.array(
+                    [[1, x * 2 + 0] for x in range(tiles)]
+                ).flatten(),
+                "Control_to_RECV_mapping": numpy.array(
+                    [[1, x * 2 + 1] for x in range(tiles)]
+                ).flatten(),
+                "Frequency_Band_RW_default": numpy.array(
+                    [["HBA_110_190", "HBA_110_190"]] * tiles
+                ).flatten(),
+            }
+        )
+        self.recv_proxy = self.setup_proxy("STAT/RECVH/H0", defaults=True)
+        self.sdpfirmware_proxy = self.setup_proxy(sdpfirmware_name)
+        self.sdp_proxy = self.setup_proxy(sdp_name)
+
+        # configure the frequencies, which allows access
+        # to the calibration attributes and commands
+        self.sdpfirmware_proxy.clock_RW = CLK_160_MHZ
+        self.recv_proxy.RCU_band_select_RW = [[1] * N_rcu_inp] * N_rcu
+
+    def test_ANT_mask_RW_configured_after_Antenna_Usage_Mask(self):
+        """Verify if ANT_mask_RW values are correctly configured from Antenna_Usage_Mask values"""
+
+        antennafield_proxy = self.proxy
+        numpy.testing.assert_equal(
+            numpy.array([[True] * N_rcu_inp] * N_rcu), self.recv_proxy.ANT_mask_RW
+        )
+
+        antenna_status = numpy.array([AntennaStatus.OK] * self.tiles)
+        antenna_use = numpy.array(
+            [AntennaUse.ON] + [AntennaUse.AUTO] * (self.tiles - 1)
+        )
+        antenna_properties = {
+            "Antenna_Status": antenna_status,
+            "Antenna_Use": antenna_use,
+        }
+        mapping_properties = {
+            "Power_to_RECV_mapping": [-1, -1] * self.tiles,
+            # Two inputs of recv device connected, only defined for 48 inputs
+            # each pair is one input
+            "Control_to_RECV_mapping": [1, 0, 1, 1] + [-1, -1] * (self.tiles - 2),
+        }
+        antennafield_proxy.off()
+        antennafield_proxy.put_property(antenna_properties)
+        antennafield_proxy.put_property(mapping_properties)
+        antennafield_proxy.boot()
+        antennafield_proxy.power_hardware_on()
+
+        # Verify all antennas are indicated to work
+        numpy.testing.assert_equal(
+            numpy.array([True] * self.tiles), antennafield_proxy.Antenna_Usage_Mask_R
+        )
+
+        # Verify only connected inputs + Antenna_Usage_Mask_R are true
+        # As well as dimensions of ANT_mask_RW must match control mapping
+        numpy.testing.assert_equal(
+            numpy.array([True] * 2 + [False] * (self.tiles - 2)),
+            antennafield_proxy.ANT_mask_RW,
+        )
+
+        # Verify recv proxy values unaffected as default for ANT_mask_RW is true
+        numpy.testing.assert_equal(
+            numpy.array([True] * 2 + [True] * (MAX_ANTENNA - 2)),
+            self.recv_proxy.ANT_mask_RW.flatten(),
+        )
+
+    def test_ANT_mask_RW_configured_after_Antenna_Usage_Mask_only_one_functioning_antenna(
+        self,
+    ):
+        """Verify if ANT_mask_RW values are correctly configured from
+        Antenna_Usage_Mask values (only second antenna is OK)"""
+
+        antennafield_proxy = self.proxy
+
+        # Broken antennas except second
+        antenna_status = numpy.array(
+            [AntennaStatus.BROKEN]
+            + [AntennaStatus.OK]
+            + [AntennaStatus.BROKEN] * (self.tiles - 2)
+        )
+        antenna_use = numpy.array([AntennaUse.AUTO] * self.tiles)
+        antenna_properties = {
+            "Antenna_Status": antenna_status,
+            "Antenna_Use": antenna_use,
+        }
+
+        # Configure control mapping to control all 96 inputs of recv device
+        mapping_properties = {
+            "Power_to_RECV_mapping": [-1, -1] * self.tiles,
+            "Control_to_RECV_mapping":
+            # [1, 0,  1, 1,  1, 2,  1, x  ...  1, 95]
+            numpy.array([[1, x] for x in range(self.tiles)]).flatten(),
+        }
+
+        # Cycle device and set properties
+        antennafield_proxy.off()
+        antennafield_proxy.put_property(antenna_properties)
+        antennafield_proxy.put_property(mapping_properties)
+        antennafield_proxy.boot()
+        antennafield_proxy.power_hardware_on()
+
+        # Antenna_Usage_Mask_R should be false except one
+        numpy.testing.assert_equal(
+            numpy.array([False] + [True] + [False] * (self.tiles - 2)),
+            antennafield_proxy.Antenna_Usage_Mask_R,
+        )
+        # device.power_hardware_on() writes Antenna_Usage_Mask_R to ANT_mask_RW
+        numpy.testing.assert_equal(
+            numpy.array([False] + [True] + [False] * (self.tiles - 2)),
+            antennafield_proxy.ANT_mask_RW,
+        )
+        # ANT_mask_RW on antennafield writes to configured recv devices for all
+        # mapped inputs. Unmapped values at the end remain at True.
+        numpy.testing.assert_equal(
+            numpy.array(
+                [False]
+                + [True]
+                + [False] * (self.tiles - 2)
+                + [True] * (MAX_ANTENNA - self.tiles)
+            ),
+            self.recv_proxy.ANT_mask_RW.flatten(),
+        )
+
+    def test_antennafield_set_mapped_attribute_ignore_all(self):
+        """Verify RECV device attribute unaffected by antennafield if not mapped"""
+
+        # Connect recvh/1 device but no control inputs
+        mapping_properties = {
+            "Power_to_RECV_mapping": [-1, -1] * self.tiles,
+            "Control_to_RECV_mapping": [-1, -1] * self.tiles,
+        }
+
+        # Cycle device an put properties
+        antennafield_proxy = self.proxy
+        antennafield_proxy.off()
+        antennafield_proxy.put_property(mapping_properties)
+        antennafield_proxy.boot()
+
+        # Set HBAT_PWR_on_RW to false on recv device and read results
+        self.recv_proxy.write_attribute(
+            "HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA
+        )
+        current_values = self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value
+
+        # write true through antennafield
+        antennafield_proxy.write_attribute(
+            "HBAT_PWR_on_RW", [[True] * N_elements * N_pol] * self.tiles
+        )
+        # Test that original recv values for HBAT_PWR_on_RW match current
+        numpy.testing.assert_equal(
+            current_values, self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value
+        )
+
+        # Verify device did not enter FAULT state
+        self.assertEqual(DevState.ON, antennafield_proxy.state())
+
+    def test_antennafield_set_mapped_attribute(self):
+        """Verify RECV device attribute changed by antennafield if mapped inputs"""
+
+        mapping_properties = {
+            "Power_to_RECV_mapping": [-1, -1] * self.tiles,
+            # Each pair is one mapping so 2 inputs are connected
+            "Control_to_RECV_mapping": [1, 0, 1, 1] + [-1, -1] * (self.tiles - 2),
+        }
+
+        antennafield_proxy = self.proxy
+        antennafield_proxy.off()
+        antennafield_proxy.put_property(mapping_properties)
+        antennafield_proxy.boot()
+
+        self.recv_proxy.write_attribute(
+            "HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA
+        )
+
+        try:
+            antennafield_proxy.write_attribute(
+                "HBAT_PWR_on_RW", [[True] * N_elements * N_pol] * self.tiles
+            )
+            numpy.testing.assert_equal(
+                numpy.array(
+                    [[True] * N_elements * N_pol] * 2
+                    + [[False] * N_elements * N_pol] * (MAX_ANTENNA - 2)
+                ),
+                self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value,
+            )
+        finally:
+            # Always restore recv again
+            self.recv_proxy.write_attribute(
+                "HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA
+            )
+
+        # Verify device did not enter FAULT state
+        self.assertEqual(DevState.ON, antennafield_proxy.state())
+
+    def test_antennafield_set_mapped_attribute_all(self):
+        """Verify RECV device attribute changed by antennafield all inputs mapped"""
+
+        mapping_properties = {
+            "Power_to_RECV_mapping": [-1, -1] * self.tiles,
+            "Control_to_RECV_mapping":
+            # [1, 0, 1, 1, 1, 2, 1, x ... 1, 95]
+            numpy.array([[1, x] for x in range(self.tiles)]).flatten(),
+        }
+
+        antennafield_proxy = self.proxy
+        antennafield_proxy.off()
+        antennafield_proxy.put_property(mapping_properties)
+        antennafield_proxy.boot()
+
+        self.recv_proxy.write_attribute(
+            "HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA
+        )
+
+        try:
+            antennafield_proxy.write_attribute(
+                "HBAT_PWR_on_RW", [[True] * N_elements * N_pol] * self.tiles
+            )
+
+            # Mapped values went to True
+            numpy.testing.assert_equal(
+                numpy.array([[True] * N_elements * N_pol] * self.tiles),
+                self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value[: self.tiles],
+            )
+            # Unmapped values went to False
+            numpy.testing.assert_equal(
+                numpy.array(
+                    [[False] * N_elements * N_pol] * (MAX_ANTENNA - self.tiles)
+                ),
+                self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value[self.tiles :],
+            )
+        finally:
+            # Always restore recv again
+            self.recv_proxy.write_attribute(
+                "HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA
+            )
+
+        # Verify device did not enter FAULT state
+        self.assertEqual(DevState.ON, antennafield_proxy.state())
+
+    def test_antennafield_set_mapped_attribute_small(self):
+        """Verify small RECV device attribute changed all inputs mapped"""
+
+        mapping_properties = {
+            "Power_to_RECV_mapping": numpy.array(  # X maps on power
+                [[1, x * 2 + 1] for x in range(self.tiles)]
+            ).flatten(),
+            "Control_to_RECV_mapping": numpy.array(  # Y maps on control
+                [[1, x * 2 + 0] for x in range(self.tiles)]
+            ).flatten(),
+        }
+
+        antennafield_proxy = self.proxy
+        antennafield_proxy.off()
+        antennafield_proxy.put_property(mapping_properties)
+        antennafield_proxy.boot()
+
+        self.recv_proxy.write_attribute("RCU_band_select_RW", [[0] * N_rcu_inp] * N_rcu)
+
+        try:
+            antennafield_proxy.write_attribute(
+                # [X, Y]
+                "RCU_band_select_RW",
+                [[1, 2]] * self.tiles,
+            )
+
+            # Mapped values were overwritten
+            numpy.testing.assert_equal(
+                # [Power, Control]
+                numpy.array([2, 1] * self.tiles),
+                self.recv_proxy.read_attribute("RCU_band_select_RW").value.flatten()[
+                    : (self.tiles * 2)
+                ],
+            )
+            # Unmapped values remain at 0
+            numpy.testing.assert_equal(
+                # [Power, Control]
+                numpy.array([0] * (MAX_ANTENNA - self.tiles * 2)),
+                self.recv_proxy.read_attribute("RCU_band_select_RW").value.flatten()[
+                    (self.tiles * 2) :
+                ],
+            )
+        finally:
+            # Always restore recv again
+            self.recv_proxy.write_attribute(
+                "RCU_band_select_RW", [[0] * N_rcu_inp] * N_rcu
+            )
+
+        # Verify device did not enter FAULT state
+        self.assertEqual(DevState.ON, antennafield_proxy.state())
+
+    def test_frequency_band(self):
+        # Test whether changes in frequency band propagate properly.
+        VALID_MODI = ["HBA_110_190", "HBA_170_230", "HBA_210_250"]
+
+        properties = {
+            "Control_to_RECV_mapping": [1, 1] + [-1, -1] * (self.tiles - 1),
+            "Power_to_RECV_mapping": [1, 0] + [-1, -1] * (self.tiles - 1),
+            "Antenna_to_SDP_Mapping": [0, 1, 0, 0] + [-1, -1] * (self.tiles - 2),
+        }
+
+        antennafield_proxy = self.proxy
+        antennafield_proxy.off()
+        antennafield_proxy.put_property(properties)
+        antennafield_proxy.boot()
+
+        for band in [b for b in bands.values() if b.name in VALID_MODI]:
+            # clear downstream settings
+            self.recv_proxy.write_attribute(
+                "RCU_band_select_RW", [[0] * N_rcu_inp] * N_rcu
+            )
+            self.sdp_proxy.write_attribute("nyquist_zone_RW", [[0] * S_pn] * N_pn)
+
+            antennafield_proxy.write_attribute(
+                "Frequency_Band_RW", [[band.name, band.name]] * self.tiles
+            )
+            # Wait for Tango attributes updating
+            time.sleep(1)
+
+            # test whether clock propagated correctly
+            numpy.testing.assert_equal(
+                band.clock, self.sdpfirmware_proxy.clock_RW, err_msg=f"{band.name}"
+            )
+
+            # test whether nyquist zone propagated correctly (for both polarisations!)
+            numpy.testing.assert_equal(
+                numpy.array(
+                    [band.nyquist_zone, band.nyquist_zone] * 2 + [0] * (N_pn * S_pn - 4)
+                ),
+                self.sdp_proxy.read_attribute("nyquist_zone_RW").value.flatten(),
+                err_msg=f"{band.name}",
+            )
+
+            # test whether rcu filter propagated correctly
+            numpy.testing.assert_equal(
+                numpy.array(
+                    [band.rcu_band, band.rcu_band] + [0] * (N_rcu_inp * N_rcu - 2)
+                ),
+                self.recv_proxy.read_attribute("RCU_band_select_RW").value.flatten(),
+                err_msg=f"{band.name}",
+            )
+
+            # test whether reading back results in the same band for our inputs
+            numpy.testing.assert_equal(
+                numpy.array([band.name, band.name]),
+                antennafield_proxy.read_attribute("Frequency_Band_RW").value[0],
+                err_msg=f"{band.name}",
+            )
diff --git a/tangostationcontrol/integration_test/common/device_observation_control_tests.py b/tangostationcontrol/integration_test/common/device_observation_control_tests.py
new file mode 100644
index 000000000..1e7caa9dc
--- /dev/null
+++ b/tangostationcontrol/integration_test/common/device_observation_control_tests.py
@@ -0,0 +1,308 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+import json
+import logging
+import time
+from datetime import datetime
+from datetime import timedelta
+
+import numpy
+from integration_test.default.devices.base import TestDeviceBase
+from tango import DevFailed, DevState
+
+from tangostationcontrol.common.constants import CS001_TILES
+from timeout_decorator import timeout_decorator
+
+logger = logging.getLogger()
+
+
+class DeviceObservationControlTests(TestDeviceBase):
+    __test__ = False
+
+    def setUp(
+        self,
+        tile_number: int = CS001_TILES,
+        hba_immediate_json: str = None,
+        antennafield_name: str = None,
+        recv_name: str = None,
+        sdp_name: str = None,
+        sdpfirmware_name: str = None,
+        beamlet_name: str = None,
+        digitalbeam_name: str = None,
+        tilebeam_name: str = None,
+        sst_name: str = None,
+        xst_name: str = None,
+        bst_name: str = None,
+    ):
+        super().setUp("STAT/ObservationControl/1")
+
+        if hba_immediate_json is None:
+            raise RuntimeError("JSON specification for immediate observation required")
+
+        self.valid_json = hba_immediate_json
+        self.expected_obs_id = json.loads(self.valid_json)["antenna_fields"][0][
+            "observation_id"
+        ]
+        self.expected_antenna_field = json.loads(self.valid_json)["antenna_fields"][0][
+            "antenna_field"
+        ]
+
+        if (
+            antennafield_name is None
+            or recv_name is None
+            or sdp_name is None
+            or sdpfirmware_name is None
+            or beamlet_name is None
+            or digitalbeam_name is None
+            or tilebeam_name is None
+            or sst_name is None
+            or xst_name is None
+            or bst_name is None
+        ):
+            raise RuntimeError("Missing required device names to initialize tests")
+
+        self.recv_proxy = self.setup_proxy(recv_name, defaults=True)
+        self.sdpfirmware_proxy = self.setup_proxy(sdpfirmware_name)
+        self.sdp_proxy = self.setup_proxy(sdp_name)
+
+        self.metadata_proxy = self.setup_proxy("STAT/Metadata/1")
+
+        control_mapping = [[1, i] for i in range(tile_number)]
+        sdp_mapping = [[i // 6, i % 6] for i in range(tile_number)]
+        self.antennafield_proxy = self.setup_proxy(
+            antennafield_name,
+            defaults=True,
+            restore_properties=True,
+            cb=lambda x: {
+                x.put_property(
+                    {
+                        "Antenna_Set": "ALL",
+                        "Power_to_RECV_mapping": numpy.array(control_mapping).flatten(),
+                        "Antenna_to_SDP_Mapping": numpy.array(sdp_mapping).flatten(),
+                    }
+                )
+            },
+        )
+
+        self.beamlet_proxy = self.setup_proxy(beamlet_name, defaults=True)
+        self.digitalbeam_proxy = self.setup_proxy(digitalbeam_name, defaults=True)
+        self.tilebeam_proxy = self.setup_proxy(tilebeam_name, defaults=True)
+        self.sst_proxy = self.setup_proxy(sst_name, defaults=True)
+        self.xst_proxy = self.setup_proxy(xst_name, defaults=True)
+        self.bst_proxy = self.setup_proxy(bst_name, defaults=True)
+
+    def on_device_assert(self, proxy):
+        """Transition the device to ON and assert intermediate states
+
+        This will repeatedly call ``stop_all_observations_now`` in turn calling
+        ``_destroy_all_observation_field_devices`` cleaning the Database from exported
+        devices
+        """
+
+        proxy.Off()
+        self.assertEqual(DevState.OFF, proxy.state())
+        proxy.Initialise()
+        self.assertEqual(DevState.STANDBY, proxy.state())
+        proxy.On()
+        self.assertEqual(DevState.ON, proxy.state())
+
+    def test_device_on(self):
+        """Transition the ObservationControl device to ON state"""
+        self.on_device_assert(self.proxy)
+
+    def test_no_observation_running(self):
+        """Assert no current observations on fresh boot"""
+
+        self.on_device_assert(self.proxy)
+        self.assertFalse(self.proxy.is_any_observation_running())
+        self.assertFalse(self.proxy.is_observation_running(self.expected_obs_id))
+        self.assertFalse(self.proxy.is_observation_running(54321))
+
+    def test_add_observation_now(self):
+        """Test starting an observation now"""
+
+        self.on_device_assert(self.proxy)
+
+        # Integration test JSON has no start time so will start immediately even when
+        # using `add_observation`
+        self.proxy.add_observation(self.valid_json)
+
+        self.assertTrue(self.proxy.is_any_observation_running())
+        self.assertTrue(self.proxy.is_observation_running(self.expected_obs_id))
+        self.assertTrue(self.proxy.is_antenna_field_active(self.expected_antenna_field))
+
+        self.proxy.stop_observation_now(self.expected_obs_id)
+
+        self.assertFalse(self.proxy.is_any_observation_running())
+        self.assertFalse(self.proxy.is_observation_running(self.expected_obs_id))
+        self.assertFalse(
+            self.proxy.is_antenna_field_active(self.expected_antenna_field)
+        )
+
+    def test_add_observation_future(self):
+        """Test starting an observation in the future"""
+
+        self.on_device_assert(self.proxy)
+
+        parameters = json.loads(self.valid_json)
+        for antenna_field in parameters["antenna_fields"]:
+            antenna_field["start_time"] = (
+                datetime.now() + timedelta(days=1)
+            ).isoformat()
+            antenna_field["stop_time"] = (
+                datetime.now() + timedelta(days=2)
+            ).isoformat()
+
+        self.proxy.add_observation(json.dumps(parameters))
+
+        self.assertIn(self.expected_obs_id, self.proxy.observations_R)
+        self.assertNotIn(self.expected_obs_id, self.proxy.running_observations_R)
+        self.assertFalse(self.proxy.is_any_observation_running())
+        self.assertFalse(self.proxy.is_observation_running(self.expected_obs_id))
+
+        self.proxy.stop_observation_now(self.expected_obs_id)
+
+    def test_add_observation_multiple(self):
+        """Test starting multiple observations"""
+
+        second_observation_json = json.loads(self.valid_json)
+        second_observation_json["antenna_fields"][0]["observation_id"] = 54321
+
+        self.on_device_assert(self.proxy)
+
+        self.proxy.add_observation(self.valid_json)
+        self.proxy.add_observation(json.dumps(second_observation_json))
+
+        self.assertTrue(self.proxy.is_any_observation_running())
+        self.assertTrue(self.proxy.is_observation_running(self.expected_obs_id))
+        self.assertTrue(self.proxy.is_observation_running(54321))
+
+        self.proxy.stop_observation_now(self.expected_obs_id)
+        self.proxy.stop_observation_now(54321)
+
+    def test_stop_observation_invalid_id(self):
+        """Test stop_observation exceptions for invalid ids"""
+
+        self.on_device_assert(self.proxy)
+
+        self.assertRaises(DevFailed, self.proxy.stop_observation_now, -1)
+
+    def test_stop_observation_invalid_running(self):
+        """Test stop_observation exceptions for not running"""
+
+        self.on_device_assert(self.proxy)
+
+        self.assertRaises(DevFailed, self.proxy.stop_observation_now, 2)
+
+    def test_is_any_observation_running_after_stop_all_observations_now(self):
+        """Test whether is_any_observation_running conforms when we start & stop an observation"""
+
+        self.on_device_assert(self.proxy)
+
+        self.proxy.add_observation(self.valid_json)
+        self.proxy.stop_all_observations_now()
+
+        # Test false
+        self.assertFalse(self.proxy.is_any_observation_running())
+
+    def test_start_stop_observation_now(self):
+        """Test starting and stopping an observation"""
+
+        self.on_device_assert(self.proxy)
+
+        # uses ID 12345
+        self.proxy.add_observation(self.valid_json)
+        self.proxy.stop_observation_now(self.expected_obs_id)
+
+        # Test false
+        self.assertFalse(self.proxy.is_observation_running(self.expected_obs_id))
+
+    def test_start_multi_stop_all_observation(self):
+        """Test starting and stopping multiple observations"""
+
+        second_observation_json = json.loads(self.valid_json)
+        second_observation_json["antenna_fields"][0]["observation_id"] = 54321
+
+        self.on_device_assert(self.proxy)
+
+        # uses ID 5
+        self.proxy.add_observation(self.valid_json)
+        self.proxy.add_observation(json.dumps(second_observation_json))
+        self.proxy.stop_all_observations_now()
+
+        # Test false
+        self.assertFalse(self.proxy.is_observation_running(self.expected_obs_id))
+        self.assertFalse(self.proxy.is_observation_running(54321))
+
+    def test_check_and_convert_parameters_invalid_id(self):
+        """Test invalid parameter detection"""
+
+        parameters = json.loads(self.valid_json)
+        for station in parameters["antenna_fields"]:
+            station["observation_id"] = -1
+
+        self.on_device_assert(self.proxy)
+        self.assertRaises(DevFailed, self.proxy.add_observation, json.dumps(parameters))
+
+    def test_check_and_convert_parameters_invalid_empty(self):
+        """Test empty parameter detection"""
+
+        self.on_device_assert(self.proxy)
+        self.assertRaises(DevFailed, self.proxy.add_observation, "{}")
+
+    def test_check_and_convert_parameters_invalid_antenna_set(self):
+        """Test invalid antenna set name"""
+        parameters = json.loads(self.valid_json)
+        for station in parameters["antenna_fields"]:
+            station["antenna_set"] = "ZZZ"
+        self.on_device_assert(self.proxy)
+        self.assertRaises(DevFailed, self.proxy.add_observation, json.dumps(parameters))
+
+    def test_check_and_convert_parameters_invalid_time(self):
+        """Test invalid parameter detection"""
+
+        parameters = json.loads(self.valid_json)
+        for antenna_field in parameters["antenna_fields"]:
+            antenna_field["stop_time"] = (
+                datetime.now() - timedelta(seconds=1)
+            ).isoformat()
+
+        self.on_device_assert(self.proxy)
+        self.assertRaises(DevFailed, self.proxy.add_observation, json.dumps(parameters))
+
+    @timeout_decorator.timeout(60)
+    def test_add_observation_callbacks(self):
+        """Test adding an observation and checking start / stop callbacks work"""
+
+        self.on_device_assert(self.proxy)
+
+        parameters = json.loads(self.valid_json)
+        for antenna_field in parameters["antenna_fields"]:
+            antenna_field["start_time"] = (
+                datetime.now() + timedelta(seconds=5)
+            ).isoformat()
+            antenna_field["stop_time"] = (
+                datetime.now() + timedelta(seconds=20)
+            ).isoformat()
+
+        self.proxy.add_observation(json.dumps(parameters))
+
+        self.assertFalse(self.proxy.is_observation_running(self.expected_obs_id))
+
+        time.sleep(5)
+
+        while not self.proxy.is_observation_running(self.expected_obs_id):
+            logging.info("Waiting for observation to start...")
+            time.sleep(0.1)
+
+        self.assertTrue(self.proxy.is_observation_running(self.expected_obs_id))
+        self.assertGreater(self.metadata_proxy.messages_published_R, 0)
+
+        time.sleep(20)
+
+        while self.proxy.is_observation_running(self.expected_obs_id):
+            logging.info("Waiting for observation to stop...")
+            time.sleep(0.1)
+
+        self.assertFalse(self.proxy.is_observation_running(self.expected_obs_id))
diff --git a/tangostationcontrol/integration_test/common/device_sdp_tests.py b/tangostationcontrol/integration_test/common/device_sdp_tests.py
new file mode 100644
index 000000000..7284cd887
--- /dev/null
+++ b/tangostationcontrol/integration_test/common/device_sdp_tests.py
@@ -0,0 +1,18 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+from integration_test.default.devices.base import TestDeviceBase
+
+
+class SDPDeviceTests(TestDeviceBase):
+    __test__ = False
+
+    def setUp(self, sdp_name: str = None, sdpfirmware_name: str = None):
+        """Intentionally recreate the device object in each test"""
+
+        if sdp_name is None or sdpfirmware_name is None:
+            raise RuntimeError("Missing required device names to initialize tests")
+
+        super().setUp(sdp_name)
+
+        self.setup_proxy(sdpfirmware_name)
diff --git a/tangostationcontrol/integration_test/common/device_sdpfirmware_tests.py b/tangostationcontrol/integration_test/common/device_sdpfirmware_tests.py
new file mode 100644
index 000000000..84ed6791d
--- /dev/null
+++ b/tangostationcontrol/integration_test/common/device_sdpfirmware_tests.py
@@ -0,0 +1,28 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+from integration_test.default.devices.base import TestDeviceBase
+
+from tangostationcontrol.common.constants import N_pn
+from tangostationcontrol.common.env_decorators import ensure_device_boots
+
+
+class SDPFirmwareDeviceTests(TestDeviceBase):
+    """Integration test class for device SDPFirmware"""
+
+    __test__ = False
+
+    def setUp(self, sdpfirmware_name: str = None):
+        """Intentionally recreate the device object in each test"""
+
+        if sdpfirmware_name is None:
+            raise RuntimeError("Missing required device names to initialize tests")
+
+        super().setUp(sdpfirmware_name)
+
+    @ensure_device_boots()
+    def test_device_sdpfirmware_read_attribute(self):
+        """Test if we can read an attribute obtained over OPC-UA"""
+        self.assertListEqual(
+            [True] * N_pn, list(self.proxy.TR_fpga_communication_error_R)
+        )
diff --git a/tangostationcontrol/integration_test/common/device_sst_tests.py b/tangostationcontrol/integration_test/common/device_sst_tests.py
new file mode 100644
index 000000000..8a42e103c
--- /dev/null
+++ b/tangostationcontrol/integration_test/common/device_sst_tests.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+import socket
+import sys
+import time
+
+from integration_test.default.devices.base import TestDeviceBase
+from tangostationcontrol.common.env_decorators import ensure_device_boots
+
+from tango import DevState
+
+
+class SSTDeviceTests(TestDeviceBase):
+    """Integration test class for device SST"""
+
+    __test__ = False
+
+    UDP_PORT = 5011
+    TCP_PORT = 5111
+
+    def setUp(self, sst_name: str = None, sdpfirmware_name: str = None):
+        """Intentionally recreate the device object in each test"""
+
+        if sst_name is None or sdpfirmware_name is None:
+            raise RuntimeError("Missing required device names to initialize tests")
+
+        super().setUp(sst_name)
+
+        self.sdpfirmware_proxy = self.setup_proxy(sdpfirmware_name, defaults=True)
+
+    @ensure_device_boots()
+    def test_device_sst_send_udp(self):
+        port_property = {"Statistics_Client_TCP_Port": "4998"}
+        self.proxy.put_property(port_property)
+
+        self.assertEqual(DevState.ON, self.proxy.state())
+
+        s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        s1.connect(("device-sst.service.consul", 5001))
+
+        # TODO(Corne): Change me into an actual SST packet
+        s1.send("Hello World!".encode("UTF-8"))
+
+        s1.close()
+
+    @ensure_device_boots()
+    def test_device_sst_connect_tcp_receive(self):
+        port_property = {"Statistics_Client_TCP_Port": "5111"}
+        self.proxy.put_property(port_property)
+
+        time.sleep(2)
+
+        s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        s1.connect(("device-sst.service.consul", self.UDP_PORT))
+
+        s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        s2.connect(("device-sst.service.consul", self.TCP_PORT))
+
+        time.sleep(2)
+
+        # TODO(Corne): Change me into an actual SST packet
+        m_data = "Hello World!".encode("UTF-8")
+        s1.send(m_data)
+
+        time.sleep(2)
+
+        data = s2.recv(sys.getsizeof(m_data))
+
+        s1.close()
+        s2.close()
+
+        self.assertEqual(m_data, data)
diff --git a/tangostationcontrol/integration_test/common/device_tilebeam_tests.py b/tangostationcontrol/integration_test/common/device_tilebeam_tests.py
new file mode 100644
index 000000000..bc71c941a
--- /dev/null
+++ b/tangostationcontrol/integration_test/common/device_tilebeam_tests.py
@@ -0,0 +1,280 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+import logging
+import datetime
+import json
+import time
+
+import numpy
+import timeout_decorator
+
+from integration_test.device_proxy import TestDeviceProxy
+from integration_test.default.devices.base import TestDeviceBase
+
+from tangostationcontrol.common.constants import (
+    CS001_TILES,
+    MAX_ANTENNA,
+    N_elements,
+    N_pol,
+)
+from tangostationcontrol.devices.base_device_classes.antennafield_device import (
+    AntennaStatus,
+    AntennaUse,
+)
+from tangostationcontrol.common.env_decorators import (
+    ensure_device_boots,
+)
+
+logger = logging.getLogger()
+
+
+class NumpyEncoder(json.JSONEncoder):
+    def default(self, obj):
+        if isinstance(obj, numpy.ndarray):
+            return obj.tolist()
+        return json.JSONEncoder.default(self, obj)
+
+
+class TileBeamDeviceTests(TestDeviceBase):
+    """Integration test class for device Tilebeam"""
+
+    __test__ = False
+
+    longMessage = True
+
+    # HBA0 / HBA1
+    LOFAR_REF_POINTING = [24, 25, 27, 29, 17, 18, 20, 21, 10, 11, 13, 14, 3, 4, 5, 7]
+    LOFAR_REF_REPEATS = 2
+
+    def setUp(
+        self,
+        tile_number: int = CS001_TILES,
+        tilebeam_name: str = None,
+        antennafield_name: str = None,
+        sdp_name: str = None,
+        sdpfirmware_name: str = None,
+        recv_name: str = None,
+    ):
+        """Setup Tilebeam"""
+
+        if (
+            tilebeam_name is None
+            or antennafield_name is None
+            or sdp_name is None
+            or sdpfirmware_name is None
+            or recv_name is None
+        ):
+            raise RuntimeError("Missing required device names to initialize tests")
+
+        self.pointing_direction = numpy.array(
+            [["J2000", "0rad", "0rad"]] * tile_number
+        ).flatten()
+
+        super().setUp(tilebeam_name)
+
+        self.tiles = tile_number
+
+        self.station_manager_proxy = self.setup_proxy("STAT/StationManager/1")
+        self.recv_proxy = self.setup_proxy(recv_name, defaults=True)
+        self.sdpfirmware_proxy = self.setup_proxy(sdpfirmware_name, defaults=True)
+        self.sdp_proxy = self.setup_proxy(sdp_name, defaults=True)
+        self.antennafield_proxy = self.setup_proxy(
+            antennafield_name,
+            cb=self.setup_antennafield_property,
+            restore_properties=True,
+        )
+
+        # check if AntennaField really exposes the expected number of tiles
+        self.assertEqual(self.tiles, self.antennafield_proxy.nr_antennas_R)
+
+    def setup_antennafield_property(self, proxy: TestDeviceProxy):
+        """Setup AntennaField"""
+        control_mapping = [[1, i] for i in range(self.tiles)]
+        antenna_status = numpy.array([AntennaStatus.OK] * MAX_ANTENNA)
+        antenna_use = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA)
+        proxy.put_property(
+            {
+                "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(),
+                "Antenna_Status": antenna_status,
+                "Antenna_Use": antenna_use,
+            }
+        )
+
+        # check if AntennaField really exposes the expected number of tiles
+        self.assertEqual(self.tiles, proxy.nr_antennas_R)
+
+    @ensure_device_boots()
+    def test_delays_dims(self):
+        """Verify delays are retrieved with correct dimensions"""
+        delays = self.proxy.delays(self.pointing_direction)
+        self.assertEqual(self.tiles * N_elements, len(delays))
+
+    @ensure_device_boots()
+    def test_set_pointing(self):
+        """Verify if set pointing procedure is correctly executed"""
+        # setup BEAM
+        self.proxy.Tracking_enabled_RW = False
+
+        # Verify attribute is present (all zeros if never used before)
+        delays_r1 = numpy.array(
+            self.antennafield_proxy.read_attribute("HBAT_BF_delay_steps_stage_RW").value
+        )
+
+        self.assertIsNotNone(delays_r1)
+
+        time.sleep(3)
+
+        # Verify writing operation does not lead to errors
+        self.proxy.set_pointing(self.pointing_direction)  # write values to RECV
+        delays_r2 = numpy.array(
+            self.antennafield_proxy.read_attribute("HBAT_BF_delay_steps_stage_RW").value
+        )
+
+        self.assertIsNotNone(delays_r2)
+
+        # Verify delays changed (to be discussed)
+        # self.assertFalse((delays_r1==delays_r2).all())
+
+    @ensure_device_boots()
+    def test_pointing_to_zenith(self):
+        self.proxy.Tracking_enabled_RW = False
+
+        # Point to Zenith
+        self.proxy.set_pointing(
+            numpy.array([["AZELGEO", "0rad", "1.570796rad"]] * self.tiles).flatten()
+        )
+
+        calculated_HBAT_delay_steps = numpy.array(
+            self.antennafield_proxy.read_attribute("HBAT_BF_delay_steps_stage_RW").value
+        )
+
+        expected_HBAT_delay_steps = numpy.array(
+            [[15] * N_elements * N_pol] * self.tiles, dtype=numpy.int64
+        )
+
+        # Accept values of 14, 15 and 16
+        numpy.testing.assert_allclose(
+            calculated_HBAT_delay_steps, expected_HBAT_delay_steps, rtol=0.066, atol=1
+        )
+
+    @ensure_device_boots()
+    def test_pointing_across_horizon(self):
+        antennafield_proxy = self.antennafield_proxy
+
+        self.proxy.Tracking_enabled_RW = False
+
+        # point at north on the horizon
+        self.proxy.set_pointing(["AZELGEO", "0rad", "0rad"] * self.tiles)
+
+        # obtain delays of the X polarisation of all the elements of the first tile
+        north_beam_delay_steps = antennafield_proxy.HBAT_BF_delay_steps_stage_RW[
+            0
+        ].reshape(4, 4, 2)[:, :, 0]
+
+        # delays must differ under rotation, or our test will give a false positive
+        self.assertNotEqual(
+            north_beam_delay_steps.tolist(),
+            numpy.rot90(north_beam_delay_steps).tolist(),
+        )
+        # 90, 180 and 270 degrees
+        for angle in (1.570796, 3.141593, 4.712389):
+            # point at angle degrees (90=E, 180=S, 270=W)
+            self.proxy.set_pointing(["AZELGEO", f"{angle}rad", "0rad"] * self.tiles)
+
+            # obtain delays of the X polarisation of all the elements of the first tile
+            angled_beam_delay_steps = antennafield_proxy.HBAT_BF_delay_steps_stage_RW[
+                0
+            ].reshape(4, 4, 2)[:, :, 0]
+
+            expected_delay_steps = numpy.rot90(
+                north_beam_delay_steps, k=-(int((angle * 180) / numpy.pi) / 90)
+            )
+
+            self.assertListEqual(
+                expected_delay_steps.tolist(),
+                angled_beam_delay_steps.tolist(),
+                msg=f"angle={angle}",
+            )
+
+    @ensure_device_boots()
+    def test_delays_same_as_LOFAR_ref_pointing(self):
+        self.proxy.Tracking_enabled_RW = False
+
+        # Point to LOFAR 1 ref pointing (0.929342, 0.952579, J2000)
+        pointings = numpy.array(
+            [["J2000", "0.929342rad", "0.952579rad"]] * self.tiles
+        ).flatten()
+        # Need to set the time to '2022-01-18 11:19:35'
+        timestamp = datetime.datetime(2022, 1, 18, 11, 19, 35).timestamp()
+
+        parameters = {"pointing_direction": pointings, "timestamp": timestamp}
+
+        json_string = json.dumps(parameters, cls=NumpyEncoder)
+        self.proxy.set_pointing_for_specific_time(json_string)
+
+        calculated_HBAT_delay_steps = numpy.array(
+            self.antennafield_proxy.read_attribute("HBAT_BF_delay_steps_stage_RW").value
+        )  # dims (self.tiles, 32)
+
+        # Check all delay steps are zero with small margin
+        # [24, 25, 27, 28, 17, 18, 20, 21, 10, 11, 13, 14, 3, 4, 5, 7] These are the real values from LOFAR.
+        # The [3] = 28 diff is explained that we match the closest delay step and LOFAR 1 wants the one with
+        # in 0.2ns but if it can't it will do a int(delay / 0.5ns) so we get slightly different results but
+        # they can be explained.
+        expected_HBAT_delay_steps = numpy.repeat(
+            numpy.array(
+                self.LOFAR_REF_POINTING,
+                dtype=numpy.int64,
+            ),
+            repeats=self.LOFAR_REF_REPEATS,
+        )
+        numpy.testing.assert_equal(
+            calculated_HBAT_delay_steps[0], expected_HBAT_delay_steps
+        )
+        numpy.testing.assert_equal(
+            calculated_HBAT_delay_steps[self.tiles - 1],
+            expected_HBAT_delay_steps,
+        )
+
+    @ensure_device_boots()
+    def test_tilebeam_tracking(self):
+        # check if we're really tracking
+        self.assertTrue(self.proxy.Tracking_enabled_R)
+
+        # point somewhere
+        new_pointings = [("J2000", f"{tile}rad", "0rad") for tile in range(self.tiles)]
+        self.proxy.Pointing_direction_RW = new_pointings
+
+        # check pointing
+        self.assertListEqual(
+            new_pointings[0:2], list(self.proxy.Pointing_direction_R[0:2])
+        )
+
+    @timeout_decorator.timeout(120)
+    def test_beam_tracking_95_percent_interval(self):
+        """Verify that the beam tracking operates within 95% of interval"""
+
+        self.proxy.initialise()
+        self.proxy.Tracking_enabled_RW = True
+        self.proxy.on()
+
+        interval = float(
+            self.proxy.get_property("Beam_tracking_interval")["Beam_tracking_interval"][
+                0
+            ]
+        )
+
+        # Allow beam tracking time to settle
+        time.sleep(interval * 3)
+
+        # We have to poll at regular interval due to not working subscribe
+        # events
+        for _ in range(0, 5):
+            error = self.proxy.Pointing_error_R[0]
+            self.assertTrue(
+                -interval * 0.05 < error < interval * 0.05,
+                f"Error: {error} larger than {interval * 0.05}",
+            )
+            logger.info("BeamTracking error: %s", error)
+            time.sleep(interval)
diff --git a/tangostationcontrol/integration_test/common/device_xst_tests.py b/tangostationcontrol/integration_test/common/device_xst_tests.py
new file mode 100644
index 000000000..5f263fe80
--- /dev/null
+++ b/tangostationcontrol/integration_test/common/device_xst_tests.py
@@ -0,0 +1,18 @@
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+from integration_test.default.devices.base import TestDeviceBase
+
+
+class XSTDeviceTests(TestDeviceBase):
+    __test__ = False
+
+    def setUp(self, xst_name: str = None, sdpfirmware_name: str = None):
+        """Intentionally recreate the device object in each test"""
+
+        if xst_name is None or sdpfirmware_name is None:
+            raise RuntimeError("Missing required device names to initialize tests")
+
+        super().setUp(xst_name)
+
+        self.sdpfirmware_proxy = self.setup_proxy(sdpfirmware_name, defaults=True)
diff --git a/tangostationcontrol/integration_test/configuration/configDB/LOFAR_ConfigDb.json b/tangostationcontrol/integration_test/configuration/configDB/LOFAR_ConfigDb.json
index a204f8347..70927173e 100644
--- a/tangostationcontrol/integration_test/configuration/configDB/LOFAR_ConfigDb.json
+++ b/tangostationcontrol/integration_test/configuration/configDB/LOFAR_ConfigDb.json
@@ -48,31 +48,6 @@
                 }
             }
         },
-        "PSOC": {
-            "STAT": {
-                "PSOC": {
-                    "STAT/PSOC/1": {
-                        "properties": {
-                            "SNMP_host": ["127.0.0.1"],
-                            "SNMP_community": ["public"],
-                            "SNMP_mib_dir": ["devices/mibs/PowerNet-MIB.mib"],
-                            "SNMP_timeout": ["10.0"],
-                            "SNMP_version": ["1"],
-                            "PSOC_sockets": [
-                                "socket_1",
-                                "socket_2",
-                                "socket_3",
-                                "socket_4",
-                                "socket_5",
-                                "socket_6",
-                                "socket_7",
-                                "socket_8"
-                            ]
-                        }
-                    }
-                }
-            }
-        },
         "PCON": {
             "STAT": {
                 "PCON": {
diff --git a/tangostationcontrol/integration_test/configuration/configDB/simulators_ConfigDb.json b/tangostationcontrol/integration_test/configuration/configDB/simulators_ConfigDb.json
index 88f571110..bd298cfb0 100644
--- a/tangostationcontrol/integration_test/configuration/configDB/simulators_ConfigDb.json
+++ b/tangostationcontrol/integration_test/configuration/configDB/simulators_ConfigDb.json
@@ -342,19 +342,6 @@
                 }
             }
         },
-        "PSOC": {
-            "STAT": {
-                "PSOC": {
-                    "STAT/PSOC/1": {
-                        "properties": {
-                            "SNMP_use_simulators": [
-                                "True"
-                            ]
-                        }
-                    }
-                }
-            }
-        },
         "RECVH": {
             "STAT": {
                 "RECVH": {
diff --git a/tangostationcontrol/integration_test/default/devices/antennafield/test_device_hba.py b/tangostationcontrol/integration_test/default/devices/antennafield/test_device_hba.py
index fa861b624..1478a4131 100644
--- a/tangostationcontrol/integration_test/default/devices/antennafield/test_device_hba.py
+++ b/tangostationcontrol/integration_test/default/devices/antennafield/test_device_hba.py
@@ -1,396 +1,20 @@
-#  Copyright (C)  2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  Copyright (C)  2024 ASTRON (Netherlands Institute for Radio Astronomy)
 #  SPDX-License-Identifier: Apache-2.0
 
-import time
-import numpy
+from tangostationcontrol.common.constants import CS001_TILES
 
-from integration_test.default.devices.base import TestDeviceBase
-from tango import DevState
+from integration_test.common.device_hba_tests import HBADeviceTests
 
-from tangostationcontrol.common.constants import (
-    N_elements,
-    MAX_ANTENNA,
-    N_pol,
-    N_rcu,
-    N_rcu_inp,
-    CS001_TILES,
-    CLK_160_MHZ,
-    N_pn,
-    S_pn,
-)
-from tangostationcontrol.common.frequency_bands import bands
-from tangostationcontrol.devices.base_device_classes.antennafield_device import (
-    AntennaStatus,
-    AntennaUse,
-)
 
-# tests will break in weird ways if our constants change beyond these constraints
-assert CS001_TILES >= 2
-assert N_pn * S_pn >= 4
-assert N_rcu_inp * N_rcu >= 2
-
-
-class TestHBADevice(TestDeviceBase):
-    """Integration test class for device Antennafield HBA"""
+class TestHBADevice(HBADeviceTests):
+    """Integration base test class for device HBA"""
 
     __test__ = True
 
     def setUp(self):
-        self.stationmanager_proxy = self.setup_proxy("STAT/StationManager/1")
-
-        # Setup will dump current properties and restore them for us
-        super().setUp("STAT/AFH/HBA0")
-
-        # Typical tests emulate 'CS001_TILES' number of antennas in
-        # the AntennaField. 'MAX_ANTENNA' is the number of inputs
-        # offered by a backing RECV device. Each antenna has
-        # N_pol (2) inputs.
-        self.proxy.put_property(
-            {
-                "Power_to_RECV_mapping": numpy.array(
-                    [[1, x * 2 + 0] for x in range(CS001_TILES)]
-                ).flatten(),
-                "Control_to_RECV_mapping": numpy.array(
-                    [[1, x * 2 + 1] for x in range(CS001_TILES)]
-                ).flatten(),
-                "Frequency_Band_RW_default": numpy.array(
-                    [["HBA_110_190", "HBA_110_190"]] * CS001_TILES
-                ).flatten(),
-            }
-        )
-        self.recv_proxy = self.setup_proxy("STAT/RECVH/H0", defaults=True)
-        self.sdpfirmware_proxy = self.setup_proxy("STAT/SDPFirmware/HBA0")
-        self.sdp_proxy = self.setup_proxy("STAT/SDP/HBA0")
-
-        # configure the frequencies, which allows access
-        # to the calibration attributes and commands
-        self.sdpfirmware_proxy.clock_RW = CLK_160_MHZ
-        self.recv_proxy.RCU_band_select_RW = [[1] * N_rcu_inp] * N_rcu
-
-    def test_ANT_mask_RW_configured_after_Antenna_Usage_Mask(self):
-        """Verify if ANT_mask_RW values are correctly configured from Antenna_Usage_Mask values"""
-
-        antennafield_proxy = self.proxy
-        numpy.testing.assert_equal(
-            numpy.array([[True] * N_rcu_inp] * N_rcu), self.recv_proxy.ANT_mask_RW
-        )
-
-        antenna_status = numpy.array([AntennaStatus.OK] * CS001_TILES)
-        antenna_use = numpy.array(
-            [AntennaUse.ON] + [AntennaUse.AUTO] * (CS001_TILES - 1)
-        )
-        antenna_properties = {
-            "Antenna_Status": antenna_status,
-            "Antenna_Use": antenna_use,
-        }
-        mapping_properties = {
-            "Power_to_RECV_mapping": [-1, -1] * CS001_TILES,
-            # Two inputs of recv device connected, only defined for 48 inputs
-            # each pair is one input
-            "Control_to_RECV_mapping": [1, 0, 1, 1] + [-1, -1] * (CS001_TILES - 2),
-        }
-        antennafield_proxy.off()
-        antennafield_proxy.put_property(antenna_properties)
-        antennafield_proxy.put_property(mapping_properties)
-        antennafield_proxy.boot()
-        antennafield_proxy.power_hardware_on()
-
-        # Verify all antennas are indicated to work
-        numpy.testing.assert_equal(
-            numpy.array([True] * CS001_TILES), antennafield_proxy.Antenna_Usage_Mask_R
-        )
-
-        # Verify only connected inputs + Antenna_Usage_Mask_R are true
-        # As well as dimensions of ANT_mask_RW must match control mapping
-        numpy.testing.assert_equal(
-            numpy.array([True] * 2 + [False] * (CS001_TILES - 2)),
-            antennafield_proxy.ANT_mask_RW,
-        )
-
-        # Verify recv proxy values unaffected as default for ANT_mask_RW is true
-        numpy.testing.assert_equal(
-            numpy.array([True] * 2 + [True] * (MAX_ANTENNA - 2)),
-            self.recv_proxy.ANT_mask_RW.flatten(),
-        )
-
-    def test_ANT_mask_RW_configured_after_Antenna_Usage_Mask_only_one_functioning_antenna(
-        self,
-    ):
-        """Verify if ANT_mask_RW values are correctly configured from
-        Antenna_Usage_Mask values (only second antenna is OK)"""
-
-        antennafield_proxy = self.proxy
-
-        # Broken antennas except second
-        antenna_status = numpy.array(
-            [AntennaStatus.BROKEN]
-            + [AntennaStatus.OK]
-            + [AntennaStatus.BROKEN] * (CS001_TILES - 2)
-        )
-        antenna_use = numpy.array([AntennaUse.AUTO] * CS001_TILES)
-        antenna_properties = {
-            "Antenna_Status": antenna_status,
-            "Antenna_Use": antenna_use,
-        }
-
-        # Configure control mapping to control all 96 inputs of recv device
-        mapping_properties = {
-            "Power_to_RECV_mapping": [-1, -1] * CS001_TILES,
-            "Control_to_RECV_mapping":
-            # [1, 0,  1, 1,  1, 2,  1, x  ...  1, 95]
-            numpy.array([[1, x] for x in range(CS001_TILES)]).flatten(),
-        }
-
-        # Cycle device and set properties
-        antennafield_proxy.off()
-        antennafield_proxy.put_property(antenna_properties)
-        antennafield_proxy.put_property(mapping_properties)
-        antennafield_proxy.boot()
-        antennafield_proxy.power_hardware_on()
-
-        # Antenna_Usage_Mask_R should be false except one
-        numpy.testing.assert_equal(
-            numpy.array([False] + [True] + [False] * (CS001_TILES - 2)),
-            antennafield_proxy.Antenna_Usage_Mask_R,
-        )
-        # device.power_hardware_on() writes Antenna_Usage_Mask_R to ANT_mask_RW
-        numpy.testing.assert_equal(
-            numpy.array([False] + [True] + [False] * (CS001_TILES - 2)),
-            antennafield_proxy.ANT_mask_RW,
-        )
-        # ANT_mask_RW on antennafield writes to configured recv devices for all
-        # mapped inputs. Unmapped values at the end remain at True.
-        numpy.testing.assert_equal(
-            numpy.array(
-                [False]
-                + [True]
-                + [False] * (CS001_TILES - 2)
-                + [True] * (MAX_ANTENNA - CS001_TILES)
-            ),
-            self.recv_proxy.ANT_mask_RW.flatten(),
-        )
-
-    def test_antennafield_set_mapped_attribute_ignore_all(self):
-        """Verify RECV device attribute unaffected by antennafield if not mapped"""
-
-        # Connect recvh/1 device but no control inputs
-        mapping_properties = {
-            "Power_to_RECV_mapping": [-1, -1] * CS001_TILES,
-            "Control_to_RECV_mapping": [-1, -1] * CS001_TILES,
-        }
-
-        # Cycle device an put properties
-        antennafield_proxy = self.proxy
-        antennafield_proxy.off()
-        antennafield_proxy.put_property(mapping_properties)
-        antennafield_proxy.boot()
-
-        # Set HBAT_PWR_on_RW to false on recv device and read results
-        self.recv_proxy.write_attribute(
-            "HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA
-        )
-        current_values = self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value
-
-        # write true through antennafield
-        antennafield_proxy.write_attribute(
-            "HBAT_PWR_on_RW", [[True] * N_elements * N_pol] * CS001_TILES
+        super().setUp(
+            tiles=CS001_TILES,
+            afh_name="STAT/AFH/HBA0",
+            sdpfirmware_name="STAT/SDPFIRMWARE/HBA0",
+            sdp_name="STAT/SDP/HBA0",
         )
-        # Test that original recv values for HBAT_PWR_on_RW match current
-        numpy.testing.assert_equal(
-            current_values, self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value
-        )
-
-        # Verify device did not enter FAULT state
-        self.assertEqual(DevState.ON, antennafield_proxy.state())
-
-    def test_antennafield_set_mapped_attribute(self):
-        """Verify RECV device attribute changed by antennafield if mapped inputs"""
-
-        mapping_properties = {
-            "Power_to_RECV_mapping": [-1, -1] * CS001_TILES,
-            # Each pair is one mapping so 2 inputs are connected
-            "Control_to_RECV_mapping": [1, 0, 1, 1] + [-1, -1] * (CS001_TILES - 2),
-        }
-
-        antennafield_proxy = self.proxy
-        antennafield_proxy.off()
-        antennafield_proxy.put_property(mapping_properties)
-        antennafield_proxy.boot()
-
-        self.recv_proxy.write_attribute(
-            "HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA
-        )
-
-        try:
-            antennafield_proxy.write_attribute(
-                "HBAT_PWR_on_RW", [[True] * N_elements * N_pol] * CS001_TILES
-            )
-            numpy.testing.assert_equal(
-                numpy.array(
-                    [[True] * N_elements * N_pol] * 2
-                    + [[False] * N_elements * N_pol] * (MAX_ANTENNA - 2)
-                ),
-                self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value,
-            )
-        finally:
-            # Always restore recv again
-            self.recv_proxy.write_attribute(
-                "HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA
-            )
-
-        # Verify device did not enter FAULT state
-        self.assertEqual(DevState.ON, antennafield_proxy.state())
-
-    def test_antennafield_set_mapped_attribute_all(self):
-        """Verify RECV device attribute changed by antennafield all inputs mapped"""
-
-        mapping_properties = {
-            "Power_to_RECV_mapping": [-1, -1] * CS001_TILES,
-            "Control_to_RECV_mapping":
-            # [1, 0, 1, 1, 1, 2, 1, x ... 1, 95]
-            numpy.array([[1, x] for x in range(CS001_TILES)]).flatten(),
-        }
-
-        antennafield_proxy = self.proxy
-        antennafield_proxy.off()
-        antennafield_proxy.put_property(mapping_properties)
-        antennafield_proxy.boot()
-
-        self.recv_proxy.write_attribute(
-            "HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA
-        )
-
-        try:
-            antennafield_proxy.write_attribute(
-                "HBAT_PWR_on_RW", [[True] * N_elements * N_pol] * CS001_TILES
-            )
-
-            # Mapped values went to True
-            numpy.testing.assert_equal(
-                numpy.array([[True] * N_elements * N_pol] * CS001_TILES),
-                self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value[:CS001_TILES],
-            )
-            # Unmapped values went to False
-            numpy.testing.assert_equal(
-                numpy.array(
-                    [[False] * N_elements * N_pol] * (MAX_ANTENNA - CS001_TILES)
-                ),
-                self.recv_proxy.read_attribute("HBAT_PWR_on_RW").value[CS001_TILES:],
-            )
-        finally:
-            # Always restore recv again
-            self.recv_proxy.write_attribute(
-                "HBAT_PWR_on_RW", [[False] * N_elements * N_pol] * MAX_ANTENNA
-            )
-
-        # Verify device did not enter FAULT state
-        self.assertEqual(DevState.ON, antennafield_proxy.state())
-
-    def test_antennafield_set_mapped_attribute_small(self):
-        """Verify small RECV device attribute changed all inputs mapped"""
-
-        mapping_properties = {
-            "Power_to_RECV_mapping": numpy.array(  # X maps on power
-                [[1, x * 2 + 1] for x in range(CS001_TILES)]
-            ).flatten(),
-            "Control_to_RECV_mapping": numpy.array(  # Y maps on control
-                [[1, x * 2 + 0] for x in range(CS001_TILES)]
-            ).flatten(),
-        }
-
-        antennafield_proxy = self.proxy
-        antennafield_proxy.off()
-        antennafield_proxy.put_property(mapping_properties)
-        antennafield_proxy.boot()
-
-        self.recv_proxy.write_attribute("RCU_band_select_RW", [[0] * N_rcu_inp] * N_rcu)
-
-        try:
-            antennafield_proxy.write_attribute(
-                # [X, Y]
-                "RCU_band_select_RW",
-                [[1, 2]] * CS001_TILES,
-            )
-
-            # Mapped values were overwritten
-            numpy.testing.assert_equal(
-                # [Power, Control]
-                numpy.array([2, 1] * CS001_TILES),
-                self.recv_proxy.read_attribute("RCU_band_select_RW").value.flatten()[
-                    : (CS001_TILES * 2)
-                ],
-            )
-            # Unmapped values remain at 0
-            numpy.testing.assert_equal(
-                # [Power, Control]
-                numpy.array([0] * (MAX_ANTENNA - CS001_TILES * 2)),
-                self.recv_proxy.read_attribute("RCU_band_select_RW").value.flatten()[
-                    (CS001_TILES * 2) :
-                ],
-            )
-        finally:
-            # Always restore recv again
-            self.recv_proxy.write_attribute(
-                "RCU_band_select_RW", [[0] * N_rcu_inp] * N_rcu
-            )
-
-        # Verify device did not enter FAULT state
-        self.assertEqual(DevState.ON, antennafield_proxy.state())
-
-    def test_frequency_band(self):
-        # Test whether changes in frequency band propagate properly.
-        VALID_MODI = ["HBA_110_190", "HBA_170_230", "HBA_210_250"]
-
-        properties = {
-            "Control_to_RECV_mapping": [1, 1] + [-1, -1] * (CS001_TILES - 1),
-            "Power_to_RECV_mapping": [1, 0] + [-1, -1] * (CS001_TILES - 1),
-            "Antenna_to_SDP_Mapping": [0, 1, 0, 0] + [-1, -1] * (CS001_TILES - 2),
-        }
-
-        antennafield_proxy = self.proxy
-        antennafield_proxy.off()
-        antennafield_proxy.put_property(properties)
-        antennafield_proxy.boot()
-
-        for band in [b for b in bands.values() if b.name in VALID_MODI]:
-            # clear downstream settings
-            self.recv_proxy.write_attribute(
-                "RCU_band_select_RW", [[0] * N_rcu_inp] * N_rcu
-            )
-            self.sdp_proxy.write_attribute("nyquist_zone_RW", [[0] * S_pn] * N_pn)
-
-            antennafield_proxy.write_attribute(
-                "Frequency_Band_RW", [[band.name, band.name]] * CS001_TILES
-            )
-            # Wait for Tango attributes updating
-            time.sleep(1)
-
-            # test whether clock propagated correctly
-            numpy.testing.assert_equal(
-                band.clock, self.sdpfirmware_proxy.clock_RW, err_msg=f"{band.name}"
-            )
-
-            # test whether nyquist zone propagated correctly (for both polarisations!)
-            numpy.testing.assert_equal(
-                numpy.array(
-                    [band.nyquist_zone, band.nyquist_zone] * 2 + [0] * (N_pn * S_pn - 4)
-                ),
-                self.sdp_proxy.read_attribute("nyquist_zone_RW").value.flatten(),
-                err_msg=f"{band.name}",
-            )
-
-            # test whether rcu filter propagated correctly
-            numpy.testing.assert_equal(
-                numpy.array(
-                    [band.rcu_band, band.rcu_band] + [0] * (N_rcu_inp * N_rcu - 2)
-                ),
-                self.recv_proxy.read_attribute("RCU_band_select_RW").value.flatten(),
-                err_msg=f"{band.name}",
-            )
-
-            # test whether reading back results in the same band for our inputs
-            numpy.testing.assert_equal(
-                numpy.array([band.name, band.name]),
-                antennafield_proxy.read_attribute("Frequency_Band_RW").value[0],
-                err_msg=f"{band.name}",
-            )
diff --git a/tangostationcontrol/integration_test/default/devices/base_device_classes/test_power_hierarchy.py b/tangostationcontrol/integration_test/default/devices/base_device_classes/test_power_hierarchy.py
index 7a904f987..2794aab0f 100644
--- a/tangostationcontrol/integration_test/default/devices/base_device_classes/test_power_hierarchy.py
+++ b/tangostationcontrol/integration_test/default/devices/base_device_classes/test_power_hierarchy.py
@@ -7,18 +7,13 @@ Power Hierarchy module integration test
 import logging
 
 import tangostationcontrol
-from tango import DevState, DeviceProxy, Database
+from tango import Database
 
 from integration_test import base
 from integration_test.device_proxy import TestDeviceProxy
-from lofar_station_client.common import CaseInsensitiveString
-from tangostationcontrol.common.constants import N_rcu, N_rcu_inp
-from tangostationcontrol.devices.base_device_classes.hierarchy_device import (
-    NotFoundException,
-    HierarchyMatchingFilter,
-)
-from tangostationcontrol.devices.base_device_classes.power_hierarchy import (
-    PowerHierarchyControlDevice,
+
+from integration_test.common.base_device_classes.power_hierarchy_tests import (
+    DevicePowerHierarchyControlTests,
 )
 
 logger = logging.getLogger()
@@ -28,7 +23,6 @@ class TestPowerAvailableInStateAttribute(base.IntegrationTestCase):
     expected = {
         "StationManager": "HIBERNATE",
         "CCD": "HIBERNATE",
-        "PSOC": "HIBERNATE",
         "PCON": "HIBERNATE",
         "SDPFirmware": "STANDBY",
         "ProtectionControl": "HIBERNATE",
@@ -71,345 +65,12 @@ class TestPowerAvailableInStateAttribute(base.IntegrationTestCase):
                 )
 
 
-class TestPowerHierarchyControlDevice(base.IntegrationTestCase):
-    """Integration Test class for PowerHierarchyDevice methods"""
-
-    pwr_attr_name = "hardware_powered_R"
-
-    stationmanager_name = "STAT/StationManager/1"
-    ec_name = "STAT/EC/1"
-    antennafield_name = "STAT/AFH/HBA0"
-    aps_l0_name = "STAT/APS/L0"
-    aps_l1_name = "STAT/APS/L1"
-    aps_h0_name = "STAT/APS/H0"
-    apsct_name = "STAT/APSCT/H0"
-    apspu_h0_name = "STAT/APSPU/H0"
-    apspu_l0_name = "STAT/APSPU/L0"
-    apspu_l1_name = "STAT/APSPU/L1"
-    ccd_name = "STAT/CCD/1"
-    pcon_name = "STAT/PCON/1"
-    psoc_name = "STAT/PSOC/1"
-    sdp_name = "STAT/SDP/HBA0"
-    unb2_h0_name = "STAT/UNB2/H0"
-    unb2_l0_name = "STAT/UNB2/L0"
-    recvh_name = "STAT/RECVH/H0"
-    recvl_name = "STAT/RECVL/L0"
-    sdp_name = "STAT/SDP/HBA0"
-    sdpfirmware_name = "STAT/SDPFirmware/HBA0"
+class TestPowerHierarchyControlDeviceHBA0(DevicePowerHierarchyControlTests):
+    __test__ = True
 
     def setUp(self):
-        super().setUp()
-        self.setup_all_devices()
-
-    def _unacceptable_exceptions(self):
-        """Return the set of exceptions raised by the last state transition
-        in the StationManager, that we do not accept.
-
-        We must accept exceptions since we do not emulate interaction with
-        actual hardware. In this function, we make sure to just ignore those
-        which we know will be raised even in a sunny-day scenario."""
-
-        result = []
-
-        for ex_str in self.stationmanager_proxy.last_requested_transition_exceptions_R:
-            # Skip "vetted" exceptions that involve switching power
-            # on actual hardware, which obviously won't power on in
-            # our simulators.
-            if "Failed to execute command_inout on device" in ex_str:
-                if "command power_hardware_on" in ex_str:
-                    continue
-                if "command power_hardware_off" in ex_str:
-                    continue
-
-            # Anything left is not acceptable
-            result.append(ex_str)
-
-        return result
-
-    def setup_stationmanager_proxy(self):
-        """Initialise StationManager device"""
-        stationmanager_proxy = TestDeviceProxy(self.stationmanager_name)
-        # extend timeout for running commands, as state transitions can take a long time
-        stationmanager_proxy.set_timeout_millis(60000)
-
-        stationmanager_proxy.off()
-        stationmanager_proxy.initialise()
-        stationmanager_proxy.on()
-        self.assertEqual(stationmanager_proxy.state(), DevState.ON)
-        return stationmanager_proxy
-
-    def setup_proxy_off(self, device_name: str):
-        """Initialise proxy and turn off device"""
-        proxy = TestDeviceProxy(device_name)
-        proxy.off()
-        return proxy
-
-    def setup_all_devices(self):
-        """Initialise all Tango devices needed for state transitions"""
-        self.stationmanager_proxy = self.setup_stationmanager_proxy()
-
-        self.ec_proxy = self.setup_proxy_off(self.ec_name)
-        self.aps_l0_proxy = self.setup_proxy_off(self.aps_l0_name)
-        self.psoc_proxy = self.setup_proxy_off(self.psoc_name)
-        self.pcon_proxy = self.setup_proxy_off(self.pcon_name)
-        self.ccd_proxy = self.setup_proxy_off(self.ccd_name)
-        self.apspu_h0_proxy = self.setup_proxy_off(self.apspu_h0_name)
-        self.apspu_l0_proxy = self.setup_proxy_off(self.apspu_l0_name)
-        self.apsct_proxy = self.setup_proxy_off(self.apsct_name)
-        self.unb2_h0_proxy = self.setup_proxy_off(self.unb2_h0_name)
-        self.unb2_l0_proxy = self.setup_proxy_off(self.unb2_l0_name)
-        self.recvh_proxy = self.setup_proxy_off(self.recvh_name)
-        self.recvl_proxy = self.setup_proxy_off(self.recvl_name)
-        self.sdpfirmware_proxy = self.setup_proxy_off(self.sdpfirmware_name)
-        self.sdp_proxy = self.setup_proxy_off(self.sdp_name)
-        self.antennafield_proxy = self.setup_proxy_off(self.antennafield_name)
+        """Intentionally recreate the device object in each test"""
 
-    def test_power_sequence_definition(self):
-        """
-        Test whether Power Sequence is correctly retrieved from the HierarchyDevice
-        """
-        self.setup_stationmanager_proxy()
-        self.setup_all_devices()
-
-        stationmanager_ph = PowerHierarchyControlDevice()
-        stationmanager_ph.init(self.stationmanager_name)
-        children_hierarchy = stationmanager_ph.children(depth=2)
-
-        # Check if EC is child of StationManager
-        ec_name = self.ec_name.casefold()
-        self.assertTrue(ec_name in children_hierarchy)
-        self.assertTrue(isinstance(children_hierarchy[ec_name]["proxy"], DeviceProxy))
-
-        # Check if PSOC is child of EC
-        self.assertTrue(
-            self.psoc_name.casefold() in children_hierarchy[ec_name]["children"].keys()
-        )
-
-        # Check if EC retrieves correctly its parent state (StationManager -> ON)
-        ec_ph = PowerHierarchyControlDevice()
-        ec_ph.init(self.ec_name)
-        self.assertEqual(ec_ph.parent_state(), DevState.ON)
-        # Check if child reads correctly a parent attribute
-        self.assertEqual(
-            ec_ph.read_parent_attribute("Station_Name_R"),
-            "CS001",
-        )
-
-    def test_off_to_hibernate(self):
-        """Test Tango devices are correctly triggered in the OFF to HIBERNATE transition"""
-        self.assertEqual(self.psoc_proxy.state(), DevState.OFF)
-        self.assertEqual(self.pcon_proxy.state(), DevState.OFF)
-        self.assertEqual(self.ccd_proxy.state(), DevState.OFF)
-        # Switch from OFF to HIBERNATE
-        self.stationmanager_proxy.station_hibernate()
-        self.assertEqual(self.stationmanager_proxy.station_state_R.name, "HIBERNATE")
-        self.assertEqual(
-            self.stationmanager_proxy.requested_station_state_R.name, "HIBERNATE"
-        )
-        self.assertEqual(self.psoc_proxy.state(), DevState.ON)
-        self.assertEqual(self.pcon_proxy.state(), DevState.ON)
-        self.assertEqual(self.ccd_proxy.state(), DevState.ON)
-
-        logger.info(
-            "Exceptions suppressed in test_off_to_hibernate: %s",
-            self.stationmanager_proxy.last_requested_transition_exceptions_R,
-        )
-
-        self.assertListEqual([], self._unacceptable_exceptions())
-
-    def test_hibernate_to_standby(self):
-        """
-        Test whether Tango devices are correctly triggered in the HIBERNATE to STANDBY transition
-        """
-        # Switch from OFF to HIBERNATE
-        self.stationmanager_proxy.station_hibernate()
-        self.assertEqual(self.stationmanager_proxy.station_state_R.name, "HIBERNATE")
-        self.assertEqual(
-            self.stationmanager_proxy.requested_station_state_R.name, "HIBERNATE"
-        )
-        self.assertEqual(self.apspu_h0_proxy.state(), DevState.OFF)
-        self.assertEqual(self.apspu_l0_proxy.state(), DevState.OFF)
-        self.assertEqual(self.apsct_proxy.state(), DevState.OFF)
-        self.assertEqual(self.unb2_h0_proxy.state(), DevState.OFF)
-        self.assertEqual(self.unb2_l0_proxy.state(), DevState.OFF)
-        self.assertEqual(self.recvh_proxy.state(), DevState.OFF)
-        self.assertEqual(self.recvl_proxy.state(), DevState.OFF)
-        self.assertEqual(self.sdpfirmware_proxy.state(), DevState.OFF)
-        # Switch from HIBERNATE to STANDBY
-        self.stationmanager_proxy.station_standby()
-        self.assertEqual(self.stationmanager_proxy.station_state_R.name, "STANDBY")
-        self.assertEqual(
-            self.stationmanager_proxy.requested_station_state_R.name, "STANDBY"
-        )
-        self.assertEqual(self.apspu_h0_proxy.state(), DevState.ON)
-        self.assertEqual(self.apspu_l0_proxy.state(), DevState.ON)
-        self.assertEqual(self.apsct_proxy.state(), DevState.ON)
-        self.assertEqual(self.unb2_h0_proxy.state(), DevState.ON)
-        self.assertEqual(self.unb2_l0_proxy.state(), DevState.ON)
-        self.assertEqual(self.recvh_proxy.state(), DevState.ON)
-        self.assertEqual(self.recvl_proxy.state(), DevState.ON)
-        self.assertEqual(self.sdpfirmware_proxy.state(), DevState.ON)
-        # Check if SDP Firmware is booted with factory image
-        firmware_images = self.sdpfirmware_proxy.FPGA_boot_image_RW.tolist()
-        self.assertListEqual(
-            [0] * len(firmware_images),
-            firmware_images,
-        )
-
-        logger.info(
-            "Exceptions suppressed: %s",
-            self.stationmanager_proxy.last_requested_transition_exceptions_R,
-        )
-
-        self.assertListEqual([], self._unacceptable_exceptions())
-
-    def test_standby_to_on(self):
-        """
-        Test whether Tango devices are correctly triggered in the STANDBY to ON transition
-        """
-        # Switch from OFF to HIBERNATE
-        self.stationmanager_proxy.station_hibernate()
-        # Switch from HIBERNATE to STANDBY
-        self.stationmanager_proxy.station_standby()
-        # Switch from STANDBY to ON
-        self.assertEqual(self.sdp_proxy.state(), DevState.OFF)
-        self.assertEqual(self.antennafield_proxy.state(), DevState.OFF)
-        self.stationmanager_proxy.station_on()
-        self.assertEqual(self.stationmanager_proxy.station_state_R.name, "ON")
-        self.assertEqual(self.stationmanager_proxy.requested_station_state_R.name, "ON")
-        self.assertEqual(self.sdp_proxy.state(), DevState.ON)
-        self.assertEqual(self.antennafield_proxy.state(), DevState.ON)
-
-        # Test if DTH and DAB are disabled
-        self.assertListEqual(
-            self.recvh_proxy.RCU_DTH_on_R.tolist(),
-            [[False] * N_rcu_inp] * N_rcu,
-        )
-        self.assertListEqual(
-            self.antennafield_proxy.RCU_DAB_filter_on_RW.tolist(),
-            [False] * self.antennafield_proxy.nr_antennas_R,
-        )
-
-        logger.info(
-            "Exceptions suppressed: %s",
-            self.stationmanager_proxy.last_requested_transition_exceptions_R,
-        )
-
-        self.assertListEqual([], self._unacceptable_exceptions())
-
-    def test_on_to_standby(self):
-        """
-        Test whether Tango devices are correctly triggered in the ON to STANDBY transition
-        """
-        # Switch from OFF to HIBERNATE
-        self.stationmanager_proxy.station_hibernate()
-        # Switch from HIBERNATE to STANDBY
-        self.stationmanager_proxy.station_standby()
-        # Switch from STANDBY to ON
-        self.stationmanager_proxy.station_on()
-        # Reverse to STANDBY
-        self.stationmanager_proxy.station_standby()
-        self.assertEqual(self.stationmanager_proxy.station_state_R.name, "STANDBY")
-        self.assertEqual(
-            self.stationmanager_proxy.requested_station_state_R.name, "STANDBY"
-        )
-        self.assertEqual(self.sdp_proxy.state(), DevState.OFF)
-        self.assertEqual(self.antennafield_proxy.state(), DevState.OFF)
-
-        logger.info(
-            "Exceptions suppressed: %s",
-            self.stationmanager_proxy.last_requested_transition_exceptions_R,
-        )
-
-        self.assertListEqual([], self._unacceptable_exceptions())
-
-    def test_standby_to_hibernate(self):
-        """
-        Test whether Tango devices are correctly triggered in the STANDBY to HIBERNATE transition
-        """
-        # Switch from OFF to HIBERNATE
-        self.stationmanager_proxy.station_hibernate()
-        # Switch from HIBERNATE to STANDBY
-        self.stationmanager_proxy.station_standby()
-        # Reverse to HIBERNATE
-        self.stationmanager_proxy.station_hibernate()
-        self.assertEqual(self.stationmanager_proxy.station_state_R.name, "HIBERNATE")
-        self.assertEqual(
-            self.stationmanager_proxy.requested_station_state_R.name, "HIBERNATE"
-        )
-        self.assertEqual(self.apspu_h0_proxy.state(), DevState.OFF)
-        self.assertEqual(self.apspu_l0_proxy.state(), DevState.OFF)
-        self.assertEqual(self.apsct_proxy.state(), DevState.OFF)
-        self.assertEqual(self.unb2_h0_proxy.state(), DevState.OFF)
-        self.assertEqual(self.unb2_l0_proxy.state(), DevState.OFF)
-        self.assertEqual(self.recvh_proxy.state(), DevState.OFF)
-        self.assertEqual(self.recvl_proxy.state(), DevState.OFF)
-        self.assertEqual(self.sdpfirmware_proxy.state(), DevState.OFF)
-
-        logger.info(
-            "Exceptions suppressed: %s",
-            self.stationmanager_proxy.last_requested_transition_exceptions_R,
-        )
-
-        self.assertListEqual([], self._unacceptable_exceptions())
-
-    def test_branch_child_deep_tree(self):
-        """
-        Test whether Tango devices are correctly retrieved with branch_child function
-        """
-        self.setup_all_devices()
-        # Create a Hierarchy Device from Device STAT/RECVH/H0
-        recvh_ph = PowerHierarchyControlDevice()
-        recvh_ph.init(self.recvh_name)
-        self.assertEqual(recvh_ph.parent(), "stat/apspu/h0")
-        # Branch child method must not return its direct parent
-        self.assertRaises(
-            NotFoundException,
-            recvh_ph.branch_child,
-            "*/apspu/h*",
-            HierarchyMatchingFilter.REGEX,
-        )
-        # Test if returns another device with the right query
-        self.assertEqual(
-            recvh_ph.branch_child("*/apspu/*", HierarchyMatchingFilter.REGEX).name(),
-            CaseInsensitiveString(self.apspu_l0_name),
-        )
-        # Test if returns a device in the tree with depth > 1
-        self.assertEqual(
-            recvh_ph.branch_child("*/aps/l*", HierarchyMatchingFilter.REGEX).name(),
-            CaseInsensitiveString(self.aps_l0_name),
-        )
-
-    def test_branch_children_names_deep_tree(self):
-        """
-        Test whether Tango devices are correctly retrieved with
-        branch_children_names function
-        """
-        self.setup_all_devices()
-        # Create a Hierarchy Device from Device STAT/RECVH/H0
-        recvh_ph = PowerHierarchyControlDevice()
-        recvh_ph.init(self.recvh_name)
-        self.assertEqual(recvh_ph.parent(), "stat/apspu/h0")
-        self.assertEqual(recvh_ph.children(), {})
-        # Branch_children_names method must not return its direct parent
-        self.assertListEqual(
-            recvh_ph.branch_children_names("*/apspu/h*", HierarchyMatchingFilter.REGEX),
-            [],
-        )
-        # Test if returns another device with the right query
-        self.assertListEqual(
-            recvh_ph.branch_children_names("*/apspu/*", HierarchyMatchingFilter.REGEX),
-            [
-                CaseInsensitiveString(self.apspu_l0_name),
-                CaseInsensitiveString(self.apspu_l1_name),
-            ],
-        )
-        # Test if returns a device in the tree with depth > 1
-        self.assertListEqual(
-            recvh_ph.branch_children_names("*/aps/*", HierarchyMatchingFilter.REGEX),
-            [
-                CaseInsensitiveString(self.aps_l0_name),
-                CaseInsensitiveString(self.aps_l1_name),
-                CaseInsensitiveString(self.aps_h0_name),
-            ],
+        super().setUp(
+            "CS001", "STAT/AFH/HBA0", "STAT/SDP/HBA0", "STAT/sdpfirmware/HBA0"
         )
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py b/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py
index 67035f2ee..013926e56 100644
--- a/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py
+++ b/tangostationcontrol/integration_test/default/devices/test_device_beamlet.py
@@ -1,129 +1,22 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
 
-import time
-from ctypes import c_short
+from integration_test.common.device_beamlet_tests import BeamletDeviceTests
 
-import numpy
-import numpy.testing
-
-from integration_test.default.devices.base import TestDeviceBase
-
-from tango import DevState
-from tangostationcontrol.common.constants import (
-    N_beamlets_ctrl,
-    S_pn,
-    CLK_200_MHZ,
-    CLK_160_MHZ,
-    N_pn,
-    A_pn,
-)
-
-
-class TestDeviceBeamlet(TestDeviceBase):
-    """Integration test class for device Beamlet"""
 
+class TestDeviceBeamletHBA0(BeamletDeviceTests):
     __test__ = True
 
     def setUp(self):
         """Intentionally recreate the device object in each test"""
-        super().setUp("STAT/Beamlet/HBA0")
-
-        self.sdp_proxy = self.setup_proxy("STAT/SDP/HBA0", defaults=True)
-        self.sdp_proxy.nyquist_zone_RW = [[2] * S_pn] * N_pn
-
-        self.sdpfirmware_proxy = self.setup_proxy(
-            "STAT/SDPFirmware/HBA0", defaults=True
-        )
-        self.sdpfirmware_proxy.clock_RW = CLK_200_MHZ
-
-    def test_pointing_to_zenith(self):
-        self.proxy.initialise()
-        self.proxy.on()
-
-        # The subband frequency of HBA subband 0 is 200 MHz,
-        # so its period is 5 ns. A delay of 2.5e-9 seconds is thus half a period,
-        # and result in a 180 degree phase offset.
-        delays = numpy.array([[[2.5e-9] * N_pn] * A_pn] * N_beamlets_ctrl)
-
-        calculated_bf_weights = self.proxy.calculate_bf_weights(delays.flatten())
-
-        # With a unit weight of 2**14, we thus expect beamformer weights of -2**14 + 0j,
-        # which is 49152 when read as an uint32.
-        self.assertEqual(-(2**14), c_short(49152).value)  # check our calculations
-        expected_bf_weights = numpy.array(
-            [49152] * N_pn * A_pn * N_beamlets_ctrl, dtype=numpy.uint32
-        )
 
-        numpy.testing.assert_almost_equal(expected_bf_weights, calculated_bf_weights)
+        super().setUp("STAT/Beamlet/HBA0", "STAT/SDP/HBA0", "STAT/sdpfirmware/HBA0")
 
-    def test_subband_select_change(self):
-        # Change subband
-        self.proxy.off()
-        self.proxy.initialise()
-        self.assertEqual(DevState.STANDBY, self.proxy.state())
-        self.proxy.subband_select_RW = [10] * N_beamlets_ctrl
-        self.proxy.on()
-        self.assertEqual(DevState.ON, self.proxy.state())
 
-        # The subband frequency of HBA subband 10 is 201953125 Hz
-        # so its period is 4.95 ns ca, half period is 2.4758e-9
-        delays = numpy.array([[[2.4758e-9] * N_pn] * A_pn] * N_beamlets_ctrl)
-        calculated_bf_weights_subband_10 = self.proxy.calculate_bf_weights(
-            delays.flatten()
-        )
-
-        self.assertEqual(-(2**14), c_short(49152).value)  # check our calculations
-        expected_bf_weights_10 = numpy.array(
-            [49152] * N_pn * A_pn * N_beamlets_ctrl, dtype=numpy.uint32
-        )
-        numpy.testing.assert_almost_equal(
-            expected_bf_weights_10, calculated_bf_weights_subband_10
-        )
-
-    def test_sdp_clock_change(self):
-        sdpfirmware_proxy = self.sdpfirmware_proxy
-        self.proxy.initialise()
-        self.proxy.subband_select_RW = numpy.array(
-            list(range(317)) + [316] + list(range(318, N_beamlets_ctrl)),
-            dtype=numpy.uint32,
-        )
-        self.proxy.on()
-
-        # any non-zero delay should result in different weights for different clocks
-        delays = numpy.array([[[2.5e-9] * N_pn] * A_pn] * N_beamlets_ctrl)
-
-        sdpfirmware_proxy.clock_RW = CLK_200_MHZ
-        time.sleep(3)  # wait for beamlet device to process change event
-        calculated_bf_weights_200 = self.proxy.calculate_bf_weights(delays.flatten())
-
-        sdpfirmware_proxy.clock_RW = CLK_160_MHZ
-        time.sleep(3)  # wait for beamlet device to process change event
-        calculated_bf_weights_160 = self.proxy.calculate_bf_weights(delays.flatten())
-
-        sdpfirmware_proxy.clock_RW = CLK_200_MHZ
-        time.sleep(3)  # wait for beamlet device to process change event
-        calculated_bf_weights_200_v2 = self.proxy.calculate_bf_weights(delays.flatten())
-
-        # outcome should be changed back and forth across clock changes
-        self.assertTrue((calculated_bf_weights_200 != calculated_bf_weights_160).all())
-        self.assertTrue(
-            (calculated_bf_weights_200 == calculated_bf_weights_200_v2).all()
-        )
+class TestDeviceBeamletHBA1(BeamletDeviceTests):
+    __test__ = True
 
-        # change subbands
-        self.proxy.off()
-        self.proxy.initialise()
-        self.proxy.subband_select_RW = [317] * N_beamlets_ctrl
-        self.proxy.on()
-        calculated_bf_weights_200_v3 = self.proxy.calculate_bf_weights(delays.flatten())
-        self.assertTrue(
-            (calculated_bf_weights_200_v2 != calculated_bf_weights_200_v3).all()
-        )
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
 
-        sdpfirmware_proxy.clock_RW = CLK_160_MHZ
-        time.sleep(1)  # wait for beamlet device to process change event
-        calculated_bf_weights_160_v2 = self.proxy.calculate_bf_weights(delays.flatten())
-        self.assertTrue(
-            (calculated_bf_weights_160 != calculated_bf_weights_160_v2).all()
-        )
+        super().setUp("STAT/Beamlet/HBA1", "STAT/SDP/HBA1", "STAT/sdpfirmware/HBA1")
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_bst.py b/tangostationcontrol/integration_test/default/devices/test_device_bst.py
index 62441c3ca..41bf97784 100644
--- a/tangostationcontrol/integration_test/default/devices/test_device_bst.py
+++ b/tangostationcontrol/integration_test/default/devices/test_device_bst.py
@@ -1,19 +1,22 @@
 # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
 
-from integration_test.default.devices.base import TestDeviceBase
+from integration_test.common.device_bst_tests import BSTDeviceTests
 
 
-class TestDeviceBST(TestDeviceBase):
+class TestDeviceBSTHBA0(BSTDeviceTests):
     __test__ = True
 
     def setUp(self):
         """Intentionally recreate the device object in each test"""
-        super().setUp("STAT/BST/HBA0")
 
-    def test_device_read_all_attributes(self):
-        # We need to connect to SDP first to read some of our attributes
-        self.setup_proxy("STAT/sdpfirmware/HBA0", defaults=True)
-        self.setup_proxy("STAT/SDP/HBA0", defaults=True)
+        super().setUp("STAT/BST/HBA0", "STAT/sdpfirmware/HBA0", "STAT/SDP/HBA0")
 
-        super().test_device_read_all_attributes()
+
+class TestDeviceBSTHBA1(BSTDeviceTests):
+    __test__ = True
+
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
+
+        super().setUp("STAT/BST/HBA1", "STAT/sdpfirmware/HBA1", "STAT/SDP/HBA1")
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py b/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py
index c57c6e221..02568e6ff 100644
--- a/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py
+++ b/tangostationcontrol/integration_test/default/devices/test_device_digitalbeam.py
@@ -1,233 +1,40 @@
-#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
-#  SPDX-License-Identifier: Apache-2.0
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
 
-import logging
-import time
+from tangostationcontrol.common.constants import CS001_TILES
 
-import numpy
-import timeout_decorator
+from integration_test.common.device_digitalbeam_tests import DigitalBeamDeviceTests
 
-from integration_test.device_proxy import TestDeviceProxy
-from integration_test.default.devices.base import TestDeviceBase
-from tangostationcontrol.common.constants import (
-    MAX_ANTENNA,
-    N_beamlets_ctrl,
-    N_pn,
-    A_pn,
-    CLK_200_MHZ,
-    CLK_160_MHZ,
-    CS001_TILES,
-)
-from tangostationcontrol.devices.base_device_classes.antennafield_device import (
-    AntennaStatus,
-    AntennaUse,
-)
 
-logger = logging.getLogger()
-
-# tests will break in weird ways if our constants change beyond these constraints
-assert CS001_TILES >= 2
-assert MAX_ANTENNA >= 2
-
-
-class TestDeviceDigitalBeam(TestDeviceBase):
+class TestDeviceDigitalBeamHBA0(DigitalBeamDeviceTests):
     __test__ = True
 
-    antenna_status_ok = numpy.array([AntennaStatus.OK] * MAX_ANTENNA)
-    antenna_status_only_second = numpy.array(
-        [AntennaStatus.BROKEN]
-        + [AntennaStatus.OK]
-        + [AntennaStatus.BROKEN] * (MAX_ANTENNA - 2)
-    )
-    antenna_use_ok = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA)
-
-    antennafield_iden = "STAT/AFH/HBA0"
-    beamlet_iden = "STAT/Beamlet/HBA0"
-    recv_iden = "STAT/RECVH/H0"
-    sdpfirmware_iden = "STAT/SDPFirmware/HBA0"
-    sdp_iden = "STAT/SDP/HBA0"
-
     def setUp(self):
         """Intentionally recreate the device object in each test"""
-        super().setUp("STAT/DigitalBeam/HBA0")
-
-        self.recv_proxy = self.setup_proxy(self.recv_iden, defaults=True)
-        self.sdpfirmware_proxy = self.setup_proxy(self.sdpfirmware_iden, defaults=True)
-        self.sdp_proxy = self.setup_proxy(self.sdp_iden)
-        self.beamlet_proxy = self.initialise_beamlet_proxy()
 
-        NR_TILES = CS001_TILES
-        control_mapping = [[1, i] for i in range(NR_TILES)]
-        sdp_mapping = [[i // 6, i % 6] for i in range(NR_TILES)]
-        self.antennafield_proxy = self.setup_proxy(
-            self.antennafield_iden,
-            cb=lambda x: {
-                x.put_property(
-                    {
-                        "Control_to_RECV_mapping": numpy.array(
-                            control_mapping
-                        ).flatten(),
-                        "Antenna_to_SDP_Mapping": numpy.array(sdp_mapping).flatten(),
-                        "Antenna_Status": self.antenna_status_ok,
-                        "Antenna_Use": self.antenna_use_ok,
-                        "Antenna_Cables": ["50m", "80m"] * (CS001_TILES // 2),
-                        "Antenna_Sets": ["FIRST", "ALL"],
-                        "Antenna_Set_Masks": [
-                            "1" + ("0" * (NR_TILES - 1)),
-                            "1" * NR_TILES,
-                        ],
-                        "Antenna_Type": "HBA",
-                    }
-                )
-            },
+        super().setUp(
+            CS001_TILES,
+            "STAT/digitalbeam/HBA0",
+            "STAT/AFH/HBA0",
+            "STAT/beamlet/HBA0",
+            "STAT/SDP/HBA0",
+            "STAT/sdpfirmware/HBA0",
+            "STAT/recvh/h0",
         )
 
-        self.addCleanup(TestDeviceProxy.test_device_turn_off, self.beamlet_iden)
-
-    def initialise_beamlet_proxy(self):
-        beamlet_proxy = TestDeviceProxy(self.beamlet_iden)
-        beamlet_proxy.off()
-        beamlet_proxy.initialise()
-        return beamlet_proxy
-
-    def test_pointing_to_zenith_clock_change(self):
-        self.beamlet_proxy.on()
-
-        # Set first (default) clock configuration
-        self.sdpfirmware_proxy.clock_RW = CLK_200_MHZ
-        time.sleep(1)
-
-        self.proxy.initialise()
-        self.proxy.Tracking_enabled_RW = False
-        self.proxy.on()
-
-        # Point to Zenith
-        self.proxy.set_pointing(
-            numpy.array(
-                [["AZELGEO", "0rad", "1.570796rad"]] * self.proxy.nr_beamlets_R
-            ).flatten()
-        )
-
-        # beam weights should now be non-zero, we don't actually check their values for correctness
-        FPGA_bf_weights_pp_clock200 = self.beamlet_proxy.FPGA_bf_weights_pp_RW.flatten()
-        self.assertNotEqual(0, sum(FPGA_bf_weights_pp_clock200))
-
-        self.beamlet_proxy = self.initialise_beamlet_proxy()
-        self.beamlet_proxy.on()
-
-        # Change clock configuration
-        self.sdpfirmware_proxy.clock_RW = CLK_160_MHZ
-        time.sleep(1)
-
-        FPGA_bf_weights_pp_clock160 = self.beamlet_proxy.FPGA_bf_weights_pp_RW.flatten()
-        # Assert some values are different
-        self.assertNotEqual(
-            sum(FPGA_bf_weights_pp_clock160), sum(FPGA_bf_weights_pp_clock200)
-        )
-
-    def test_pointing_to_zenith_subband_change(self):
-        self.beamlet_proxy.subband_select_RW = numpy.array(
-            list(range(317)) + [316] + list(range(318, N_beamlets_ctrl)),
-            dtype=numpy.uint32,
-        )
-        self.beamlet_proxy.on()
-
-        self.proxy.initialise()
-        self.proxy.Tracking_enabled_RW = False
-        self.proxy.on()
-
-        # Point to Zenith
-        self.proxy.set_pointing(
-            numpy.array(
-                [["AZELGEO", "0rad", "1.570796rad"]] * self.proxy.nr_beamlets_R
-            ).flatten()
-        )
-        # Store values with first subband configuration
-        FPGA_bf_weights_pp_subband_v1 = (
-            self.beamlet_proxy.FPGA_bf_weights_pp_RW.flatten()
-        )
 
-        # Restart beamlet proxy
-        self.beamlet_proxy = self.initialise_beamlet_proxy()
-        self.beamlet_proxy.subband_select_RW = [317] * N_beamlets_ctrl
-        self.beamlet_proxy.on()
-
-        # Store values with second subband configuration
-        FPGA_bf_weights_pp_subband_v2 = (
-            self.beamlet_proxy.FPGA_bf_weights_pp_RW.flatten()
-        )
-        # Assert some values are different
-        self.assertNotEqual(
-            sum(FPGA_bf_weights_pp_subband_v1), sum(FPGA_bf_weights_pp_subband_v2)
-        )
-
-    def test_set_pointing_masked_enable(self):
-        """Verify that only selected inputs are written"""
-
-        self.proxy.initialise()
-        self.proxy.Tracking_enabled_RW = False
-        self.proxy.on()
-
-        # Enable first input
-        self.proxy.Antenna_Set_RW = "FIRST"
-
-        # fill weights with values the beamformer will never use (|x| > 1)
-        impossible_values = numpy.array([[2] * N_beamlets_ctrl * A_pn] * N_pn)
-        self.beamlet_proxy.FPGA_bf_weights_pp_RW = impossible_values
-
-        self.proxy.set_pointing(
-            numpy.array(
-                [["AZELGEO", "0rad", "1.570796rad"]] * self.proxy.nr_beamlets_R
-            ).flatten()
-        )
-
-        # Verify all impossible values are replaced with other values for all inputs
-        # which should be non-zero for the first antenna, and zero for the rest
-        FPGA_bf_weights_pp_RW = self.beamlet_proxy.FPGA_bf_weights_pp_RW.reshape(
-            (N_pn * A_pn, -1)
-        )
-
-        # first antenna should have values from the beamformer, so not 0 and not 2
-        self.assertTrue(
-            numpy.all(numpy.not_equal(0, FPGA_bf_weights_pp_RW[0, :])),
-            f"{FPGA_bf_weights_pp_RW}",
-        )
-        self.assertTrue(
-            numpy.all(numpy.not_equal(2, FPGA_bf_weights_pp_RW[0, :])),
-            f"{FPGA_bf_weights_pp_RW}",
-        )
-
-        # rest of the antennas should have been given a weight of 0, as they
-        # were not in the usage mask.
-        self.assertTrue(
-            numpy.all(numpy.equal(0, FPGA_bf_weights_pp_RW[1:CS001_TILES, :])),
-            f"{FPGA_bf_weights_pp_RW}",
-        )
-
-    @timeout_decorator.timeout(15)
-    def test_beam_tracking_90_percent_interval(self):
-        """Verify that the beam tracking operates within 95% of interval"""
+class TestDeviceDigitalBeamHBA1(DigitalBeamDeviceTests):
+    __test__ = True
 
-        self.proxy.initialise()
-        self.proxy.Tracking_enabled_RW = True
-        self.proxy.on()
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
 
-        interval = float(
-            self.proxy.get_property("Beam_tracking_interval")["Beam_tracking_interval"][
-                0
-            ]
+        super().setUp(
+            CS001_TILES,
+            "STAT/digitalbeam/HBA1",
+            "STAT/AFH/HBA1",
+            "STAT/beamlet/HBA1",
+            "STAT/SDP/HBA1",
+            "STAT/sdpfirmware/HBA1",
+            "STAT/recvh/h0",
         )
-
-        # Allow beam tracking time to settle
-        time.sleep(interval * 3)
-
-        # We have to poll at regular interval due to not working subscribe
-        # events
-        for _ in range(0, 5):
-            error = self.proxy.Pointing_error_R[0]
-            self.assertTrue(
-                -interval * 0.10 < error < interval * 0.10,
-                f"Error: {error} larger than {interval * 0.10}",
-            )
-            logger.info("BeamTracking error: %s", error)
-            time.sleep(interval)
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_metadata.py b/tangostationcontrol/integration_test/default/devices/test_device_metadata.py
index 23d2ef5d1..e1716ba39 100644
--- a/tangostationcontrol/integration_test/default/devices/test_device_metadata.py
+++ b/tangostationcontrol/integration_test/default/devices/test_device_metadata.py
@@ -13,7 +13,6 @@ class TestMetadataDevice(TestDeviceBase):
 
     def setUp(self):
         super().setUp("STAT/Metadata/1")
-        self.psoc_proxy = self.setup_proxy("STAT/PSOC/1")
 
     def test_send_metadata(self):
         """Turn on the device and emit metadata"""
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py b/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py
index bf253955a..e46c0743f 100644
--- a/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py
+++ b/tangostationcontrol/integration_test/default/devices/test_device_observation_control.py
@@ -1,326 +1,35 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
 
-import json
-import logging
-import time
-from datetime import datetime
-from datetime import timedelta
-
-import numpy
-from integration_test.default.devices.base import TestDeviceBase
-from tango import DevFailed, DevState
-
 from tangostationcontrol.common.constants import CS001_TILES
 from tangostationcontrol.test.dummy_observation_settings import (
-    get_observation_settings_hba_immediate,
+    get_observation_settings_hba_core_immediate,
 )
-from timeout_decorator import timeout_decorator
 
-logger = logging.getLogger()
+from integration_test.common.device_observation_control_tests import (
+    DeviceObservationControlTests,
+)
 
 
-class TestObservationControlDevice(TestDeviceBase):
+class TestDeviceObservationControlHBA0(DeviceObservationControlTests):
     __test__ = True
 
-    ANTENNA_TO_SDP_MAPPING = [
-        "0",
-        "0",
-        "0",
-        "1",
-        "0",
-        "2",
-        "0",
-        "3",
-        "0",
-        "4",
-        "0",
-        "5",
-        "1",
-        "0",
-        "1",
-        "1",
-        "1",
-        "2",
-        "1",
-        "3",
-        "1",
-        "4",
-        "1",
-        "5",
-        "2",
-        "0",
-        "2",
-        "1",
-        "2",
-        "2",
-        "2",
-        "3",
-        "2",
-        "4",
-        "2",
-        "5",
-        "3",
-        "0",
-        "3",
-        "1",
-        "3",
-        "2",
-        "3",
-        "3",
-        "3",
-        "4",
-        "3",
-        "5",
-    ]
-    antennafield_name = "STAT/AFH/HBA0"
-
-    VALID_JSON = get_observation_settings_hba_immediate().to_json()
-    EXPECTED_OBS_ID = json.loads(VALID_JSON)["antenna_fields"][0]["observation_id"]
-    EXPECTED_ANTENNA_FIELD = json.loads(VALID_JSON)["antenna_fields"][0][
-        "antenna_field"
-    ]
-
     def setUp(self):
-        super().setUp("STAT/ObservationControl/1")
-        self.recv_proxy = self.setup_proxy("STAT/RECVH/H0", defaults=True)
-        self.sdpfirmware_proxy = self.setup_proxy("STAT/SDPFirmware/HBA0")
-        self.sdp_proxy = self.setup_proxy("STAT/SDP/HBA0")
-        self.metadata_proxy = self.setup_proxy("STAT/Metadata/1")
-
-        control_mapping = [[1, i] for i in range(CS001_TILES)]
-        self.antennafield_proxy = self.setup_proxy(
-            self.antennafield_name,
-            defaults=True,
-            restore_properties=True,
-            cb=lambda x: {
-                x.put_property(
-                    {
-                        "Antenna_Set": "ALL",
-                        "Power_to_RECV_mapping": numpy.array(control_mapping).flatten(),
-                        "Antenna_to_SDP_Mapping": self.ANTENNA_TO_SDP_MAPPING,
-                    }
-                )
-            },
-        )
-
-        self.beamlet_proxy = self.setup_proxy("STAT/Beamlet/HBA0", defaults=True)
-        self.digitalbeam_proxy = self.setup_proxy(
-            "STAT/DigitalBeam/HBA0", defaults=True
-        )
-        self.tilebeam_proxy = self.setup_proxy("STAT/TileBeam/HBA0", defaults=True)
-
-    def on_device_assert(self, proxy):
-        """Transition the device to ON and assert intermediate states
-
-        This will repeatedly call ``stop_all_observations_now`` in turn calling
-        ``_destroy_all_observation_field_devices`` cleaning the Database from exported
-        devices
-        """
-
-        proxy.Off()
-        self.assertEqual(DevState.OFF, proxy.state())
-        proxy.Initialise()
-        self.assertEqual(DevState.STANDBY, proxy.state())
-        proxy.On()
-        self.assertEqual(DevState.ON, proxy.state())
-
-    def test_device_on(self):
-        """Transition the ObservationControl device to ON state"""
-        self.on_device_assert(self.proxy)
-
-    def test_no_observation_running(self):
-        """Assert no current observations on fresh boot"""
-
-        self.on_device_assert(self.proxy)
-        self.assertFalse(self.proxy.is_any_observation_running())
-        self.assertFalse(self.proxy.is_observation_running(self.EXPECTED_OBS_ID))
-        self.assertFalse(self.proxy.is_observation_running(54321))
-
-    def test_add_observation_now(self):
-        """Test starting an observation now"""
-
-        self.on_device_assert(self.proxy)
-
-        # Integration test JSON has no start time so will start immediately even when
-        # using `add_observation`
-        self.proxy.add_observation(self.VALID_JSON)
-
-        self.assertTrue(self.proxy.is_any_observation_running())
-        self.assertTrue(self.proxy.is_observation_running(self.EXPECTED_OBS_ID))
-        self.assertTrue(self.proxy.is_antenna_field_active(self.EXPECTED_ANTENNA_FIELD))
-
-        self.proxy.stop_observation_now(self.EXPECTED_OBS_ID)
-
-        self.assertFalse(self.proxy.is_any_observation_running())
-        self.assertFalse(self.proxy.is_observation_running(self.EXPECTED_OBS_ID))
-        self.assertFalse(
-            self.proxy.is_antenna_field_active(self.EXPECTED_ANTENNA_FIELD)
+        """Intentionally recreate the device object in each test"""
+
+        json = get_observation_settings_hba_core_immediate().to_json()
+
+        super().setUp(
+            CS001_TILES,
+            json,
+            "STAT/AFH/HBA0",
+            "STAT/recvh/h0",
+            "STAT/SDP/HBA0",
+            "STAT/sdpfirmware/HBA0",
+            "STAT/beamlet/HBA0",
+            "STAT/digitalbeam/HBA0",
+            "STAT/tilebeam/HBA0",
+            "STAT/sst/HBA0",
+            "STAT/xst/HBA0",
+            "STAT/bst/HBA0",
         )
-
-    def test_add_observation_future(self):
-        """Test starting an observation in the future"""
-
-        self.on_device_assert(self.proxy)
-
-        parameters = json.loads(self.VALID_JSON)
-        for antenna_field in parameters["antenna_fields"]:
-            antenna_field["start_time"] = (
-                datetime.now() + timedelta(days=1)
-            ).isoformat()
-            antenna_field["stop_time"] = (
-                datetime.now() + timedelta(days=2)
-            ).isoformat()
-
-        self.proxy.add_observation(json.dumps(parameters))
-
-        self.assertIn(self.EXPECTED_OBS_ID, self.proxy.observations_R)
-        self.assertNotIn(self.EXPECTED_OBS_ID, self.proxy.running_observations_R)
-        self.assertFalse(self.proxy.is_any_observation_running())
-        self.assertFalse(self.proxy.is_observation_running(self.EXPECTED_OBS_ID))
-
-        self.proxy.stop_observation_now(self.EXPECTED_OBS_ID)
-
-    def test_add_observation_multiple(self):
-        """Test starting multiple observations"""
-
-        second_observation_json = json.loads(self.VALID_JSON)
-        second_observation_json["antenna_fields"][0]["observation_id"] = 54321
-
-        self.on_device_assert(self.proxy)
-
-        self.proxy.add_observation(self.VALID_JSON)
-        self.proxy.add_observation(json.dumps(second_observation_json))
-
-        self.assertTrue(self.proxy.is_any_observation_running())
-        self.assertTrue(self.proxy.is_observation_running(self.EXPECTED_OBS_ID))
-        self.assertTrue(self.proxy.is_observation_running(54321))
-
-        self.proxy.stop_observation_now(self.EXPECTED_OBS_ID)
-        self.proxy.stop_observation_now(54321)
-
-    def test_stop_observation_invalid_id(self):
-        """Test stop_observation exceptions for invalid ids"""
-
-        self.on_device_assert(self.proxy)
-
-        self.assertRaises(DevFailed, self.proxy.stop_observation_now, -1)
-
-    def test_stop_observation_invalid_running(self):
-        """Test stop_observation exceptions for not running"""
-
-        self.on_device_assert(self.proxy)
-
-        self.assertRaises(DevFailed, self.proxy.stop_observation_now, 2)
-
-    def test_is_any_observation_running_after_stop_all_observations_now(self):
-        """Test whether is_any_observation_running conforms when we start & stop an observation"""
-
-        self.on_device_assert(self.proxy)
-
-        self.proxy.add_observation(self.VALID_JSON)
-        self.proxy.stop_all_observations_now()
-
-        # Test false
-        self.assertFalse(self.proxy.is_any_observation_running())
-
-    def test_start_stop_observation_now(self):
-        """Test starting and stopping an observation"""
-
-        self.on_device_assert(self.proxy)
-
-        # uses ID 12345
-        self.proxy.add_observation(self.VALID_JSON)
-        self.proxy.stop_observation_now(self.EXPECTED_OBS_ID)
-
-        # Test false
-        self.assertFalse(self.proxy.is_observation_running(self.EXPECTED_OBS_ID))
-
-    def test_start_multi_stop_all_observation(self):
-        """Test starting and stopping multiple observations"""
-
-        second_observation_json = json.loads(self.VALID_JSON)
-        second_observation_json["antenna_fields"][0]["observation_id"] = 54321
-
-        self.on_device_assert(self.proxy)
-
-        # uses ID 5
-        self.proxy.add_observation(self.VALID_JSON)
-        self.proxy.add_observation(json.dumps(second_observation_json))
-        self.proxy.stop_all_observations_now()
-
-        # Test false
-        self.assertFalse(self.proxy.is_observation_running(self.EXPECTED_OBS_ID))
-        self.assertFalse(self.proxy.is_observation_running(54321))
-
-    def test_check_and_convert_parameters_invalid_id(self):
-        """Test invalid parameter detection"""
-
-        parameters = json.loads(self.VALID_JSON)
-        for station in parameters["antenna_fields"]:
-            station["observation_id"] = -1
-
-        self.on_device_assert(self.proxy)
-        self.assertRaises(DevFailed, self.proxy.add_observation, json.dumps(parameters))
-
-    def test_check_and_convert_parameters_invalid_empty(self):
-        """Test empty parameter detection"""
-
-        self.on_device_assert(self.proxy)
-        self.assertRaises(DevFailed, self.proxy.add_observation, "{}")
-
-    def test_check_and_convert_parameters_invalid_antenna_set(self):
-        """Test invalid antenna set name"""
-        parameters = json.loads(self.VALID_JSON)
-        for station in parameters["antenna_fields"]:
-            station["antenna_set"] = "ZZZ"
-        self.on_device_assert(self.proxy)
-        self.assertRaises(DevFailed, self.proxy.add_observation, json.dumps(parameters))
-
-    def test_check_and_convert_parameters_invalid_time(self):
-        """Test invalid parameter detection"""
-
-        parameters = json.loads(self.VALID_JSON)
-        for antenna_field in parameters["antenna_fields"]:
-            antenna_field["stop_time"] = (
-                datetime.now() - timedelta(seconds=1)
-            ).isoformat()
-
-        self.on_device_assert(self.proxy)
-        self.assertRaises(DevFailed, self.proxy.add_observation, json.dumps(parameters))
-
-    @timeout_decorator.timeout(60)
-    def test_add_observation_callbacks(self):
-        """Test adding an observation and checking start / stop callbacks work"""
-
-        self.on_device_assert(self.proxy)
-
-        parameters = json.loads(self.VALID_JSON)
-        for antenna_field in parameters["antenna_fields"]:
-            antenna_field["start_time"] = (
-                datetime.now() + timedelta(seconds=5)
-            ).isoformat()
-            antenna_field["stop_time"] = (
-                datetime.now() + timedelta(seconds=20)
-            ).isoformat()
-
-        self.proxy.add_observation(json.dumps(parameters))
-
-        self.assertFalse(self.proxy.is_observation_running(self.EXPECTED_OBS_ID))
-
-        time.sleep(5)
-
-        while not self.proxy.is_observation_running(self.EXPECTED_OBS_ID):
-            logging.info("Waiting for observation to start...")
-            time.sleep(0.1)
-
-        self.assertTrue(self.proxy.is_observation_running(self.EXPECTED_OBS_ID))
-        self.assertGreater(self.metadata_proxy.messages_published_R, 0)
-
-        time.sleep(20)
-
-        while self.proxy.is_observation_running(self.EXPECTED_OBS_ID):
-            logging.info("Waiting for observation to stop...")
-            time.sleep(0.1)
-
-        self.assertFalse(self.proxy.is_observation_running(self.EXPECTED_OBS_ID))
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_observation_field.py b/tangostationcontrol/integration_test/default/devices/test_device_observation_field.py
index 812d065a2..1b0e78653 100644
--- a/tangostationcontrol/integration_test/default/devices/test_device_observation_field.py
+++ b/tangostationcontrol/integration_test/default/devices/test_device_observation_field.py
@@ -27,7 +27,7 @@ from tangostationcontrol.devices.base_device_classes.antennafield_device import
 )
 
 from tangostationcontrol.test.dummy_observation_settings import (
-    get_observation_settings_hba_immediate,
+    get_observation_settings_hba_core_immediate,
 )
 from tangostationcontrol.common.env_decorators import (
     restore_properties_for_devices,
@@ -108,7 +108,7 @@ class TestDeviceObservationField(TestDeviceBase):
     def setUp(self):
         super().setUp("STAT/ObservationField/1")
         self.VALID_JSON = json.dumps(
-            json.loads(get_observation_settings_hba_immediate().to_json())[
+            json.loads(get_observation_settings_hba_core_immediate().to_json())[
                 "antenna_fields"
             ][0]
         )
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_psoc.py b/tangostationcontrol/integration_test/default/devices/test_device_psoc.py
deleted file mode 100644
index e8152f668..000000000
--- a/tangostationcontrol/integration_test/default/devices/test_device_psoc.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
-
-from integration_test.default.devices.base import TestDeviceBase
-
-
-class TestDevicePSOC(TestDeviceBase):
-    __test__ = True
-
-    def setUp(self):
-        super().setUp("STAT/PSOC/1")
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_sdp.py b/tangostationcontrol/integration_test/default/devices/test_device_sdp.py
index 26b31d061..633d3f1bd 100644
--- a/tangostationcontrol/integration_test/default/devices/test_device_sdp.py
+++ b/tangostationcontrol/integration_test/default/devices/test_device_sdp.py
@@ -1,14 +1,28 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
+#  Copyright (C)  2024 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
 
-from integration_test.default.devices.base import TestDeviceBase
+from integration_test.common.device_sdp_tests import SDPDeviceTests
 
 
-class TestDeviceSDP(TestDeviceBase):
+class TestSDPDeviceHBA0(SDPDeviceTests):
+    """Integration base test class for device HBA0"""
+
     __test__ = True
 
     def setUp(self):
-        """Intentionally recreate the device object in each test"""
-        super().setUp("STAT/SDP/HBA0")
+        super().setUp(
+            sdp_name="STAT/SDP/HBA0",
+            sdpfirmware_name="STAT/SDPFIRMWARE/HBA0",
+        )
+
+
+class TestSDPDeviceHBA1(SDPDeviceTests):
+    """Integration base test class for device HBA1"""
 
-        self.setup_proxy("STAT/SDPFirmware/HBA0")
+    __test__ = True
+
+    def setUp(self):
+        super().setUp(
+            sdp_name="STAT/SDP/HBA1",
+            sdpfirmware_name="STAT/SDPFIRMWARE/HBA1",
+        )
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_sdpfirmware.py b/tangostationcontrol/integration_test/default/devices/test_device_sdpfirmware.py
index ef2b1c28c..0c10055e6 100644
--- a/tangostationcontrol/integration_test/default/devices/test_device_sdpfirmware.py
+++ b/tangostationcontrol/integration_test/default/devices/test_device_sdpfirmware.py
@@ -1,24 +1,26 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
+#  Copyright (C)  2024 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
 
-from integration_test.default.devices.base import TestDeviceBase
+from integration_test.common.device_sdpfirmware_tests import SDPFirmwareDeviceTests
 
-from tangostationcontrol.common.constants import N_pn
-from tangostationcontrol.common.env_decorators import ensure_device_boots
 
+class TestSDPFirmwareDeviceHBA0(SDPFirmwareDeviceTests):
+    """Integration base test class for device HBA0"""
 
-class TestDeviceSDPFirmware(TestDeviceBase):
-    """Integration test class for device SDPFirmware"""
+    __test__ = True
+
+    def setUp(self):
+        super().setUp(
+            sdpfirmware_name="STAT/SDPFIRMWARE/HBA0",
+        )
+
+
+class TestSDPFirmwareDeviceHBA1(SDPFirmwareDeviceTests):
+    """Integration base test class for device HBA1"""
 
     __test__ = True
 
     def setUp(self):
-        """Intentionally recreate the device object in each test"""
-        super().setUp("STAT/SDPFirmware/HBA0")
-
-    @ensure_device_boots()
-    def test_device_sdpfirmware_read_attribute(self):
-        """Test if we can read an attribute obtained over OPC-UA"""
-        self.assertListEqual(
-            [True] * N_pn, list(self.proxy.TR_fpga_communication_error_R)
+        super().setUp(
+            sdpfirmware_name="STAT/SDPFIRMWARE/HBA1",
         )
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_sst.py b/tangostationcontrol/integration_test/default/devices/test_device_sst.py
index 52244b2b4..421cbb8bc 100644
--- a/tangostationcontrol/integration_test/default/devices/test_device_sst.py
+++ b/tangostationcontrol/integration_test/default/devices/test_device_sst.py
@@ -1,68 +1,25 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
 
-import socket
-import sys
-import time
+from integration_test.common.device_sst_tests import SSTDeviceTests
 
-from integration_test.default.devices.base import TestDeviceBase
-from tangostationcontrol.common.env_decorators import ensure_device_boots
-
-from tango import DevState
-
-
-class TestDeviceSST(TestDeviceBase):
-    """Integration test class for device SST"""
 
+class TestDeviceSSTHBA0(SSTDeviceTests):
     __test__ = True
 
     def setUp(self):
         """Intentionally recreate the device object in each test"""
-        super().setUp("STAT/SST/HBA0")
-
-        self.sdpfirmware_proxy = self.setup_proxy(
-            "STAT/SDPFirmware/HBA0", defaults=True
-        )
-
-    @ensure_device_boots()
-    def test_device_sst_send_udp(self):
-        port_property = {"Statistics_Client_TCP_Port": "4998"}
-        self.proxy.put_property(port_property)
-
-        self.assertEqual(DevState.ON, self.proxy.state())
-
-        s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-        s1.connect(("device-sst.service.consul", 5001))
-
-        # TODO(Corne): Change me into an actual SST packet
-        s1.send("Hello World!".encode("UTF-8"))
 
-        s1.close()
+        super().setUp("STAT/SST/HBA0", "STAT/sdpfirmware/HBA0")
 
-    @ensure_device_boots()
-    def test_device_sst_connect_tcp_receive(self):
-        port_property = {"Statistics_Client_TCP_Port": "5111"}
-        self.proxy.put_property(port_property)
 
-        time.sleep(2)
-
-        s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-        s1.connect(("device-sst.service.consul", 5011))
-
-        s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        s2.connect(("device-sst.service.consul", 5111))
-
-        time.sleep(2)
-
-        # TODO(Corne): Change me into an actual SST packet
-        m_data = "Hello World!".encode("UTF-8")
-        s1.send(m_data)
-
-        time.sleep(2)
+class TestDeviceSSTHBA1(SSTDeviceTests):
+    __test__ = True
 
-        data = s2.recv(sys.getsizeof(m_data))
+    UDP_PORT = 5021
+    TCP_PORT = 5121
 
-        s1.close()
-        s2.close()
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
 
-        self.assertEqual(m_data, data)
+        super().setUp("STAT/SST/HBA1", "STAT/sdpfirmware/HBA1")
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py b/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py
index fc9e208e1..c07aceb9f 100644
--- a/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py
+++ b/tangostationcontrol/integration_test/default/devices/test_device_tilebeam.py
@@ -1,256 +1,38 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
 
-import logging
-import datetime
-import json
-import time
+from tangostationcontrol.common.constants import CS001_TILES
 
-import numpy
-import timeout_decorator
+from integration_test.common.device_tilebeam_tests import TileBeamDeviceTests
 
-from integration_test.device_proxy import TestDeviceProxy
-from integration_test.default.devices.base import TestDeviceBase
-
-from tangostationcontrol.common.constants import (
-    CS001_TILES,
-    MAX_ANTENNA,
-    N_elements,
-    N_pol,
-)
-from tangostationcontrol.devices.base_device_classes.antennafield_device import (
-    AntennaStatus,
-    AntennaUse,
-)
-from tangostationcontrol.common.env_decorators import (
-    ensure_device_boots,
-    restore_properties_for_devices,
-)
-
-logger = logging.getLogger()
-
-
-class NumpyEncoder(json.JSONEncoder):
-    def default(self, obj):
-        if isinstance(obj, numpy.ndarray):
-            return obj.tolist()
-        return json.JSONEncoder.default(self, obj)
-
-
-class TestDeviceTileBeam(TestDeviceBase):
-    """Integration test class for device Tilebeam"""
 
+class TestDeviceTilebeamHBA0(TileBeamDeviceTests):
     __test__ = True
 
-    antennafield_name = "STAT/AFH/HBA0"
-    POINTING_DIRECTION = numpy.array(
-        [["J2000", "0rad", "0rad"]] * CS001_TILES
-    ).flatten()
-
-    @restore_properties_for_devices([antennafield_name])
     def setUp(self):
-        """Setup Tilebeam"""
-        super().setUp("STAT/TileBeam/HBA0")
-
-        self.station_manager_proxy = self.setup_proxy("STAT/StationManager/1")
-        self.recv_proxy = self.setup_proxy("STAT/RECVH/H0", defaults=True)
-        self.sdpfirmware_proxy = self.setup_proxy(
-            "STAT/SDPFirmware/HBA0", defaults=True
-        )
-        self.sdp_proxy = self.setup_proxy("STAT/SDP/HBA0", defaults=True)
-        self.antennafield_proxy = self.setup_proxy(
-            self.antennafield_name, cb=self.setup_antennafield_property
-        )
-
-        # check if AntennaField really exposes the expected number of tiles
-        self.assertEqual(CS001_TILES, self.antennafield_proxy.nr_antennas_R)
-
-    def setup_antennafield_property(self, proxy: TestDeviceProxy):
-        """Setup AntennaField"""
-        control_mapping = [[1, i] for i in range(CS001_TILES)]
-        antenna_status = numpy.array([AntennaStatus.OK] * MAX_ANTENNA)
-        antenna_use = numpy.array([AntennaUse.AUTO] * MAX_ANTENNA)
-        proxy.put_property(
-            {
-                "Control_to_RECV_mapping": numpy.array(control_mapping).flatten(),
-                "Antenna_Status": antenna_status,
-                "Antenna_Use": antenna_use,
-            }
-        )
-
-        # check if AntennaField really exposes the expected number of tiles
-        self.assertEqual(CS001_TILES, proxy.nr_antennas_R)
-
-    @ensure_device_boots()
-    def test_delays_dims(self):
-        """Verify delays are retrieved with correct dimensions"""
-        delays = self.proxy.delays(self.POINTING_DIRECTION)
-        self.assertEqual(CS001_TILES * N_elements, len(delays))
-
-    @ensure_device_boots()
-    def test_set_pointing(self):
-        """Verify if set pointing procedure is correctly executed"""
-        # setup BEAM
-        self.proxy.Tracking_enabled_RW = False
-
-        # Verify attribute is present (all zeros if never used before)
-        delays_r1 = numpy.array(
-            self.antennafield_proxy.read_attribute("HBAT_BF_delay_steps_stage_RW").value
-        )
-
-        self.assertIsNotNone(delays_r1)
-
-        time.sleep(3)
-
-        # Verify writing operation does not lead to errors
-        self.proxy.set_pointing(self.POINTING_DIRECTION)  # write values to RECV
-        delays_r2 = numpy.array(
-            self.antennafield_proxy.read_attribute("HBAT_BF_delay_steps_stage_RW").value
-        )
-
-        self.assertIsNotNone(delays_r2)
-
-        # Verify delays changed (to be discussed)
-        # self.assertFalse((delays_r1==delays_r2).all())
-
-    @ensure_device_boots()
-    def test_pointing_to_zenith(self):
-        self.proxy.Tracking_enabled_RW = False
-
-        # Point to Zenith
-        self.proxy.set_pointing(
-            numpy.array([["AZELGEO", "0rad", "1.570796rad"]] * CS001_TILES).flatten()
-        )
-
-        calculated_HBAT_delay_steps = numpy.array(
-            self.antennafield_proxy.read_attribute("HBAT_BF_delay_steps_stage_RW").value
-        )
-
-        expected_HBAT_delay_steps = numpy.array(
-            [[15] * N_elements * N_pol] * CS001_TILES, dtype=numpy.int64
-        )
-
-        numpy.testing.assert_equal(
-            calculated_HBAT_delay_steps, expected_HBAT_delay_steps
-        )
-
-    @ensure_device_boots()
-    def test_pointing_across_horizon(self):
-        antennafield_proxy = self.antennafield_proxy
-
-        self.proxy.Tracking_enabled_RW = False
-
-        # point at north on the horizon
-        self.proxy.set_pointing(["AZELGEO", "0rad", "0rad"] * CS001_TILES)
-
-        # obtain delays of the X polarisation of all the elements of the first tile
-        north_beam_delay_steps = antennafield_proxy.HBAT_BF_delay_steps_stage_RW[
-            0
-        ].reshape(4, 4, 2)[:, :, 0]
-
-        # delays must differ under rotation, or our test will give a false positive
-        self.assertNotEqual(
-            north_beam_delay_steps.tolist(),
-            numpy.rot90(north_beam_delay_steps).tolist(),
-        )
-        # 90, 180 and 270 degrees
-        for angle in (1.570796, 3.141593, 4.712389):
-            # point at angle degrees (90=E, 180=S, 270=W)
-            self.proxy.set_pointing(["AZELGEO", f"{angle}rad", "0rad"] * CS001_TILES)
-
-            # obtain delays of the X polarisation of all the elements of the first tile
-            angled_beam_delay_steps = antennafield_proxy.HBAT_BF_delay_steps_stage_RW[
-                0
-            ].reshape(4, 4, 2)[:, :, 0]
-
-            expected_delay_steps = numpy.rot90(
-                north_beam_delay_steps, k=-(int((angle * 180) / numpy.pi) / 90)
-            )
-
-            self.assertListEqual(
-                expected_delay_steps.tolist(),
-                angled_beam_delay_steps.tolist(),
-                msg=f"angle={angle}",
-            )
+        """Intentionally recreate the device object in each test"""
 
-    @ensure_device_boots()
-    def test_delays_same_as_LOFAR_ref_pointing(self):
-        self.proxy.Tracking_enabled_RW = False
-
-        # Point to LOFAR 1 ref pointing (0.929342, 0.952579, J2000)
-        pointings = numpy.array(
-            [["J2000", "0.929342rad", "0.952579rad"]] * CS001_TILES
-        ).flatten()
-        # Need to set the time to '2022-01-18 11:19:35'
-        timestamp = datetime.datetime(2022, 1, 18, 11, 19, 35).timestamp()
-
-        parameters = {"pointing_direction": pointings, "timestamp": timestamp}
-
-        json_string = json.dumps(parameters, cls=NumpyEncoder)
-        self.proxy.set_pointing_for_specific_time(json_string)
-
-        calculated_HBAT_delay_steps = numpy.array(
-            self.antennafield_proxy.read_attribute("HBAT_BF_delay_steps_stage_RW").value
-        )  # dims (CS001_TILES, 32)
-
-        # Check all delay steps are zero with small margin
-        # [24, 25, 27, 28, 17, 18, 20, 21, 10, 11, 13, 14, 3, 4, 5, 7] These are the real values from LOFAR.
-        # The [3] = 28 diff is explained that we match the closest delay step and LOFAR 1 wants the one with
-        # in 0.2ns but if it can't it will do a int(delay / 0.5ns) so we get slightly different results but
-        # they can be explained.
-        expected_HBAT_delay_steps = numpy.repeat(
-            numpy.array(
-                [24, 25, 27, 29, 17, 18, 20, 21, 10, 11, 13, 14, 3, 4, 5, 7],
-                dtype=numpy.int64,
-            ),
-            2,
-        )
-        numpy.testing.assert_equal(
-            calculated_HBAT_delay_steps[0], expected_HBAT_delay_steps
-        )
-        numpy.testing.assert_equal(
-            calculated_HBAT_delay_steps[CS001_TILES - 1],
-            expected_HBAT_delay_steps,
+        super().setUp(
+            CS001_TILES,
+            "STAT/tilebeam/HBA0",
+            "STAT/AFH/HBA0",
+            "STAT/SDP/HBA0",
+            "STAT/sdpfirmware/HBA0",
+            "STAT/recvh/h0",
         )
 
-    @ensure_device_boots()
-    def test_tilebeam_tracking(self):
-        # check if we're really tracking
-        self.assertTrue(self.proxy.Tracking_enabled_R)
 
-        # point somewhere
-        new_pointings = [("J2000", f"{tile}rad", "0rad") for tile in range(CS001_TILES)]
-        self.proxy.Pointing_direction_RW = new_pointings
-
-        # check pointing
-        self.assertListEqual(
-            new_pointings[0:2], list(self.proxy.Pointing_direction_R[0:2])
-        )
-
-    @timeout_decorator.timeout(120)
-    def test_beam_tracking_95_percent_interval(self):
-        """Verify that the beam tracking operates within 95% of interval"""
+class TestDeviceTilebeamHBA1(TileBeamDeviceTests):
+    __test__ = True
 
-        self.proxy.initialise()
-        self.proxy.Tracking_enabled_RW = True
-        self.proxy.on()
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
 
-        interval = float(
-            self.proxy.get_property("Beam_tracking_interval")["Beam_tracking_interval"][
-                0
-            ]
+        super().setUp(
+            CS001_TILES,
+            "STAT/tilebeam/HBA1",
+            "STAT/AFH/HBA1",
+            "STAT/SDP/HBA1",
+            "STAT/sdpfirmware/HBA1",
+            "STAT/recvh/h0",
         )
-
-        # Allow beam tracking time to settle
-        time.sleep(interval * 3)
-
-        # We have to poll at regular interval due to not working subscribe
-        # events
-        for _ in range(0, 5):
-            error = self.proxy.Pointing_error_R[0]
-            self.assertTrue(
-                -interval * 0.05 < error < interval * 0.05,
-                f"Error: {error} larger than {interval * 0.05}",
-            )
-            logger.info("BeamTracking error: %s", error)
-            time.sleep(interval)
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_unb2.py b/tangostationcontrol/integration_test/default/devices/test_device_unb2.py
index 2b0575da5..8d2860f99 100644
--- a/tangostationcontrol/integration_test/default/devices/test_device_unb2.py
+++ b/tangostationcontrol/integration_test/default/devices/test_device_unb2.py
@@ -4,7 +4,7 @@
 from integration_test.default.devices.base import TestDeviceBase
 
 
-class TestDeviceUNB2(TestDeviceBase):
+class TestDeviceUNB2H0(TestDeviceBase):
     __test__ = True
 
     def setUp(self):
diff --git a/tangostationcontrol/integration_test/default/devices/test_device_xst.py b/tangostationcontrol/integration_test/default/devices/test_device_xst.py
index 9b9ff37dd..4bd88c53a 100644
--- a/tangostationcontrol/integration_test/default/devices/test_device_xst.py
+++ b/tangostationcontrol/integration_test/default/devices/test_device_xst.py
@@ -1,16 +1,22 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
 
-from integration_test.default.devices.base import TestDeviceBase
+from integration_test.common.device_xst_tests import XSTDeviceTests
 
 
-class TestDeviceXST(TestDeviceBase):
+class TestDeviceXSTHBA0(XSTDeviceTests):
     __test__ = True
 
     def setUp(self):
         """Intentionally recreate the device object in each test"""
-        super().setUp("STAT/XST/HBA0")
 
-        self.sdpfirmware_proxy = self.setup_proxy(
-            "STAT/SDPFirmware/HBA0", defaults=True
-        )
+        super().setUp("STAT/XST/HBA0", "STAT/sdpfirmware/HBA0")
+
+
+class TestDeviceXSTHBA1(XSTDeviceTests):
+    __test__ = True
+
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
+
+        super().setUp("STAT/XST/HBA1", "STAT/sdpfirmware/HBA1")
diff --git a/tangostationcontrol/integration_test/default/devices/test_observation_client.py b/tangostationcontrol/integration_test/default/devices/test_observation_client.py
index 58576f3c2..fb1290d53 100644
--- a/tangostationcontrol/integration_test/default/devices/test_observation_client.py
+++ b/tangostationcontrol/integration_test/default/devices/test_observation_client.py
@@ -10,7 +10,7 @@ from lofar_station_client.observation.station_observation import (
 from integration_test import base
 from integration_test.device_proxy import TestDeviceProxy
 from tangostationcontrol.test.dummy_observation_settings import (
-    get_observation_settings_hba_immediate,
+    get_observation_settings_hba_core_immediate,
 )
 
 from tango import DevState
@@ -57,7 +57,9 @@ class TestObservation(base.IntegrationTestCase):
         """Test of the observation_wrapper class basic functionality"""
 
         # convert the JSON specification to a dict for this class
-        specification_dict = loads(get_observation_settings_hba_immediate().to_json())
+        specification_dict = loads(
+            get_observation_settings_hba_core_immediate().to_json()
+        )
 
         # create an observation class using the dict and as host just get it using a
         # util function
diff --git a/tangostationcontrol/integration_test/remote_station/__init__.py b/tangostationcontrol/integration_test/remote_station/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tangostationcontrol/integration_test/remote_station/antennafield/__init__.py b/tangostationcontrol/integration_test/remote_station/antennafield/__init__.py
new file mode 100644
index 000000000..68ddd5cdc
--- /dev/null
+++ b/tangostationcontrol/integration_test/remote_station/antennafield/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
diff --git a/tangostationcontrol/integration_test/remote_station/antennafield/test_device_hba.py b/tangostationcontrol/integration_test/remote_station/antennafield/test_device_hba.py
new file mode 100644
index 000000000..ea842bf02
--- /dev/null
+++ b/tangostationcontrol/integration_test/remote_station/antennafield/test_device_hba.py
@@ -0,0 +1,20 @@
+#  Copyright (C)  2024 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
+from tangostationcontrol.common.constants import RS307_TILES
+
+from integration_test.common.device_hba_tests import HBADeviceTests
+
+
+class TestHBADevice(HBADeviceTests):
+    """Integration base test class for device HBA"""
+
+    __test__ = True
+
+    def setUp(self):
+        super().setUp(
+            tiles=RS307_TILES,
+            afh_name="STAT/AFH/HBA",
+            sdpfirmware_name="STAT/SDPFIRMWARE/HBA",
+            sdp_name="STAT/SDP/HBA",
+        )
diff --git a/tangostationcontrol/integration_test/remote_station/base_device_classes/__init__.py b/tangostationcontrol/integration_test/remote_station/base_device_classes/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tangostationcontrol/integration_test/remote_station/base_device_classes/test_power_hierarchy.py b/tangostationcontrol/integration_test/remote_station/base_device_classes/test_power_hierarchy.py
new file mode 100644
index 000000000..efe116924
--- /dev/null
+++ b/tangostationcontrol/integration_test/remote_station/base_device_classes/test_power_hierarchy.py
@@ -0,0 +1,21 @@
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+"""Power Hierarchy HBA integration test"""
+
+import logging
+
+from integration_test.common.base_device_classes.power_hierarchy_tests import (
+    DevicePowerHierarchyControlTests,
+)
+
+logger = logging.getLogger()
+
+
+class TestPowerHierarchyControlDeviceHBA(DevicePowerHierarchyControlTests):
+    __test__ = True
+
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
+
+        super().setUp("RS307", "STAT/AFH/HBA", "STAT/SDP/HBA", "STAT/sdpfirmware/HBA")
diff --git a/tangostationcontrol/integration_test/remote_station/test_device_beamlet.py b/tangostationcontrol/integration_test/remote_station/test_device_beamlet.py
new file mode 100644
index 000000000..d2a550d59
--- /dev/null
+++ b/tangostationcontrol/integration_test/remote_station/test_device_beamlet.py
@@ -0,0 +1,11 @@
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+from integration_test.common.device_beamlet_tests import BeamletDeviceTests
+
+
+class TestDeviceBeamletHBA(BeamletDeviceTests):
+    __test__ = True
+
+    def setUp(self):
+        super().setUp("STAT/Beamlet/HBA", "STAT/SDP/HBA", "STAT/sdpfirmware/HBA")
diff --git a/tangostationcontrol/integration_test/remote_station/test_device_bst.py b/tangostationcontrol/integration_test/remote_station/test_device_bst.py
new file mode 100644
index 000000000..4d7515cde
--- /dev/null
+++ b/tangostationcontrol/integration_test/remote_station/test_device_bst.py
@@ -0,0 +1,13 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+from integration_test.common.device_bst_tests import BSTDeviceTests
+
+
+class TestDeviceBSTHBA(BSTDeviceTests):
+    __test__ = True
+
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
+
+        super().setUp("STAT/BST/HBA", "STAT/sdpfirmware/HBA", "STAT/SDP/HBA")
diff --git a/tangostationcontrol/integration_test/remote_station/test_device_digitalbeam.py b/tangostationcontrol/integration_test/remote_station/test_device_digitalbeam.py
new file mode 100644
index 000000000..7a170b2bf
--- /dev/null
+++ b/tangostationcontrol/integration_test/remote_station/test_device_digitalbeam.py
@@ -0,0 +1,23 @@
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+from tangostationcontrol.common.constants import RS307_TILES
+
+from integration_test.common.device_digitalbeam_tests import DigitalBeamDeviceTests
+
+
+class TestDeviceDigitalBeamHBA(DigitalBeamDeviceTests):
+    __test__ = True
+
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
+
+        super().setUp(
+            RS307_TILES,
+            "STAT/digitalbeam/HBA",
+            "STAT/AFH/HBA",
+            "STAT/beamlet/HBA",
+            "STAT/SDP/HBA",
+            "STAT/sdpfirmware/HBA",
+            "STAT/recvh/h0",
+        )
diff --git a/tangostationcontrol/integration_test/remote_station/test_device_observation_control.py b/tangostationcontrol/integration_test/remote_station/test_device_observation_control.py
new file mode 100644
index 000000000..bed51697c
--- /dev/null
+++ b/tangostationcontrol/integration_test/remote_station/test_device_observation_control.py
@@ -0,0 +1,37 @@
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+from tangostationcontrol.common.constants import RS307_TILES
+from tangostationcontrol.test.dummy_observation_settings import (
+    get_observation_settings_hba_core_immediate,
+)
+
+from integration_test.common.device_observation_control_tests import (
+    DeviceObservationControlTests,
+)
+
+
+class TestDeviceObservationControlHBA(DeviceObservationControlTests):
+    __test__ = True
+
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
+
+        obs_data = get_observation_settings_hba_core_immediate()
+        obs_data.station = "rs307"
+        obs_data.antenna_fields[0].antenna_field = "HBA"
+
+        super().setUp(
+            RS307_TILES,
+            obs_data.to_json(),
+            "STAT/AFH/HBA",
+            "STAT/recvh/h0",
+            "STAT/SDP/HBA",
+            "STAT/sdpfirmware/HBA",
+            "STAT/beamlet/HBA",
+            "STAT/digitalbeam/HBA",
+            "STAT/tilebeam/HBA",
+            "STAT/sst/HBA",
+            "STAT/xst/HBA",
+            "STAT/bst/HBA",
+        )
diff --git a/tangostationcontrol/integration_test/remote_station/test_device_sdp.py b/tangostationcontrol/integration_test/remote_station/test_device_sdp.py
new file mode 100644
index 000000000..657904c1a
--- /dev/null
+++ b/tangostationcontrol/integration_test/remote_station/test_device_sdp.py
@@ -0,0 +1,16 @@
+#  Copyright (C)  2024 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
+from integration_test.common.device_sdp_tests import SDPDeviceTests
+
+
+class TestSDPDeviceHBA(SDPDeviceTests):
+    """Integration base test class for device HBA"""
+
+    __test__ = True
+
+    def setUp(self):
+        super().setUp(
+            sdp_name="STAT/SDP/HBA",
+            sdpfirmware_name="STAT/SDPFIRMWARE/HBA",
+        )
diff --git a/tangostationcontrol/integration_test/remote_station/test_device_sdpfirmware.py b/tangostationcontrol/integration_test/remote_station/test_device_sdpfirmware.py
new file mode 100644
index 000000000..788ad90e8
--- /dev/null
+++ b/tangostationcontrol/integration_test/remote_station/test_device_sdpfirmware.py
@@ -0,0 +1,15 @@
+#  Copyright (C)  2024 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
+from integration_test.common.device_sdpfirmware_tests import SDPFirmwareDeviceTests
+
+
+class TestSDPFirmwareDeviceHBA(SDPFirmwareDeviceTests):
+    """Integration base test class for device HBA"""
+
+    __test__ = True
+
+    def setUp(self):
+        super().setUp(
+            sdpfirmware_name="STAT/SDPFIRMWARE/HBA",
+        )
diff --git a/tangostationcontrol/integration_test/remote_station/test_device_sst.py b/tangostationcontrol/integration_test/remote_station/test_device_sst.py
new file mode 100644
index 000000000..44661fa7b
--- /dev/null
+++ b/tangostationcontrol/integration_test/remote_station/test_device_sst.py
@@ -0,0 +1,13 @@
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+from integration_test.common.device_sst_tests import SSTDeviceTests
+
+
+class TestDeviceBSTHBA(SSTDeviceTests):
+    __test__ = True
+
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
+
+        super().setUp("STAT/SST/HBA", "STAT/sdpfirmware/HBA")
diff --git a/tangostationcontrol/integration_test/remote_station/test_device_tilebeam.py b/tangostationcontrol/integration_test/remote_station/test_device_tilebeam.py
new file mode 100644
index 000000000..c8c783daf
--- /dev/null
+++ b/tangostationcontrol/integration_test/remote_station/test_device_tilebeam.py
@@ -0,0 +1,40 @@
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+from tangostationcontrol.common.constants import RS307_TILES
+
+from integration_test.common.device_tilebeam_tests import TileBeamDeviceTests
+
+
+class TestDeviceTilebeamHBA(TileBeamDeviceTests):
+    __test__ = True
+
+    def setUp(self):
+        super().setUp(
+            RS307_TILES,
+            "STAT/tilebeam/HBA",
+            "STAT/AFH/HBA",
+            "STAT/SDP/HBA",
+            "STAT/sdpfirmware/HBA",
+            "STAT/recvh/h0",
+        )
+
+        self.LOFAR_REF_POINTING = [
+            21,
+            24,
+            27,
+            30,
+            14,
+            17,
+            20,
+            23,
+            8,
+            11,
+            14,
+            17,
+            1,
+            4,
+            7,
+            10,
+        ]
+        self.LOFAR_REF_REPEATS = 2
diff --git a/tangostationcontrol/integration_test/remote_station/test_device_xst.py b/tangostationcontrol/integration_test/remote_station/test_device_xst.py
new file mode 100644
index 000000000..43c486feb
--- /dev/null
+++ b/tangostationcontrol/integration_test/remote_station/test_device_xst.py
@@ -0,0 +1,13 @@
+# Copyright (C) 2024 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+from integration_test.common.device_xst_tests import XSTDeviceTests
+
+
+class TestDeviceXSTHBA(XSTDeviceTests):
+    __test__ = True
+
+    def setUp(self):
+        """Intentionally recreate the device object in each test"""
+
+        super().setUp("STAT/XST/HBA", "STAT/sdpfirmware/HBA")
diff --git a/tangostationcontrol/tangostationcontrol/clients/snmp/attribute_classes.py b/tangostationcontrol/tangostationcontrol/clients/snmp/attribute_classes.py
index 3dbe6c163..7d2e60ade 100644
--- a/tangostationcontrol/tangostationcontrol/clients/snmp/attribute_classes.py
+++ b/tangostationcontrol/tangostationcontrol/clients/snmp/attribute_classes.py
@@ -165,86 +165,6 @@ class SNMPAttribute:
         return result
 
 
-class PSOC_sim:
-    """
-    This class mimics the desired behavior of the PSOC for the points we
-    are interested in. It is a replacement for the SNMPAttribute attribute class
-    """
-
-    sockets_states = 8 * [True]
-
-    def __init__(
-        self,
-        comm=None,
-        mib=None,
-        name=None,
-        idx=None,
-        dtype=None,
-        dim_x=None,
-        dim_y=None,
-        scaling_factor=1,
-    ):
-        if None in (name, idx, dim_x, dim_y):
-            raise Exception("Expected a value for argument name, idx dim_x and dim_y")
-
-        self.name = name
-
-        if dim_x == 0:
-            dim_x = 1
-        if dim_y == 0:
-            dim_y = 1
-
-        self.len = dim_y * dim_x
-        self.idx = idx - 1
-
-    def write_function(self, value):
-        """Simulated attribute write function"""
-        # we only write to sPDUOutletCtl and only as a scalar
-        if isinstance(value, list):
-            value = value[0]
-
-        if self.name == "sPDUOutletCtl":
-            if value == "outletOn":
-                PSOC_sim.sockets_states[self.idx] = True
-            elif value == "outletOff":
-                PSOC_sim.sockets_states[self.idx] = False
-            else:
-                raise ValueError(
-                    "Only acceptable values are: 'outletOn' and 'outletOff'"
-                )
-        else:
-            raise NameError(
-                f"pcon does not have any write attributes named: {self.name}"
-            )
-
-    def read_function(self):
-        """Simulated attribute read function"""
-        if self.name == "sPDUOutletCtl":
-            return_val = [
-                "outletOn" if PSOC_sim.sockets_states[i + self.idx] else "outletOff"
-                for i in range(self.len)
-            ]
-
-            if self.len == 1:
-                return return_val[0]
-            return return_val
-
-        if self.name == "sPDUMasterState":
-            return_val = ""
-            return return_val.join(
-                "On  " if i else "Off " for i in PSOC_sim.sockets_states
-            )
-
-        if self.name == "sysUpTime":
-            # return arbitrary value of 60s uptime
-            return 60000
-
-        if self.name == "rPDULoadStatusLoad":
-            # give a random value in amps
-            return 2
-        raise NameError(f"pcon does not have any read attributes named: {self.name}")
-
-
 class PCON_sim:
     """
     This class mimics the desired behavior of the PCON for the points we
diff --git a/tangostationcontrol/tangostationcontrol/common/constants.py b/tangostationcontrol/tangostationcontrol/common/constants.py
index 300d5863b..55af5593c 100644
--- a/tangostationcontrol/tangostationcontrol/common/constants.py
+++ b/tangostationcontrol/tangostationcontrol/common/constants.py
@@ -121,6 +121,9 @@ DEFAULT_N_HBA_TILES = 48
 # number of tiles in the CS001 station
 CS001_TILES = 24
 
+# number of tiles in the RS307 station
+RS307_TILES = 48
+
 # The uint32 weight representing 1.0 for the fixed-point math in SDP's packed cint16 weights for subbands and beam forming.
 SDP_UNIT_WEIGHT = 2**14
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/__init__.py b/tangostationcontrol/tangostationcontrol/devices/__init__.py
index ff3d864da..ae7dbd414 100644
--- a/tangostationcontrol/tangostationcontrol/devices/__init__.py
+++ b/tangostationcontrol/tangostationcontrol/devices/__init__.py
@@ -14,7 +14,6 @@ from .observation_field import ObservationField
 from .observation_control import ObservationControl
 from .metadata import Metadata
 from .pcon import PCON
-from .psoc import PSOC
 from .recv.recvh import RECVH
 from .recv.recvl import RECVL
 from .sdp.beamlet import Beamlet
@@ -44,7 +43,6 @@ __all__ = [
     "ObservationControl",
     "Metadata",
     "PCON",
-    "PSOC",
     "RECVL",
     "RECVH",
     "Beamlet",
diff --git a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/power_hierarchy.py b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/power_hierarchy.py
index 6fdd418b2..9e750b124 100644
--- a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/power_hierarchy.py
+++ b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/power_hierarchy.py
@@ -96,12 +96,6 @@ class PowerHierarchyControlDevice(AbstractHierarchyDevice):
             # TODO(JDM): wait for CCDTR to be powered on before booting it?
             self._boot_device(device)
 
-            # PSOC: Power on CCD
-            if device_class_matches(device, "PSOC"):
-                logger.info("Powering on %s: CCD", device)
-                device.power_hardware_on()
-                logger.info("Powering on %s: Succesful: CCD", device)
-
             # CCD: Power on clock
             if device_class_matches(device, "CCD"):
                 logger.info("Powering on %s: Clock", device)
diff --git a/tangostationcontrol/tangostationcontrol/devices/psoc.py b/tangostationcontrol/tangostationcontrol/devices/psoc.py
deleted file mode 100644
index 17375fc7f..000000000
--- a/tangostationcontrol/tangostationcontrol/devices/psoc.py
+++ /dev/null
@@ -1,180 +0,0 @@
-#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
-#  SPDX-License-Identifier: Apache-2.0
-
-""" PSOC Device Server for LOFAR2.0
-
-"""
-
-import logging
-from datetime import timedelta
-
-import numpy
-from attribute_wrapper.attribute_wrapper import AttributeWrapper
-from tango.server import device_property, command
-from tangostationcontrol.clients.snmp.attribute_classes import PSOC_sim
-
-# Additional import
-from tangostationcontrol.common.lofar_logging import (
-    device_logging_to_python,
-)
-from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES
-from tangostationcontrol.devices.base_device_classes.snmp_device import SNMPDevice
-from tangostationcontrol.common.device_decorators import only_in_states
-from tangostationcontrol.metrics import device_metrics
-
-logger = logging.getLogger()
-
-__all__ = ["PSOC"]
-
-
-@device_logging_to_python()
-@device_metrics()
-class PSOC(SNMPDevice):
-    """PSOC Device Server for LOFAR2.0"""
-
-    # number of sockets the psoc has
-    PSOC_SOCKETS = 8
-
-    # -----------------
-    # Device Properties
-    # -----------------
-    PSOC_sockets = device_property(dtype=[str], mandatory=True)
-
-    # ----------
-    # Attributes
-    # ----------
-    sockets_state_R = AttributeWrapper(
-        comms_annotation={"mib": "PowerNet-MIB", "name": "sPDUOutletCtl", "index": 1},
-        dims=(8,),
-        datatype=str,
-    )
-    master_state_R = AttributeWrapper(
-        comms_annotation={"mib": "PowerNet-MIB", "name": "sPDUMasterState"},
-        datatype=str,
-    )
-    current_load_R = AttributeWrapper(
-        comms_annotation={
-            "mib": "PowerNet-MIB",
-            "name": "rPDULoadStatusLoad",
-            "index": 1,
-        },
-        datatype=numpy.int64,
-    )
-    uptime_R = AttributeWrapper(
-        comms_annotation={"mib": "SNMPv2-MIB", "name": "sysUpTime"},
-        datatype=numpy.int64,
-    )
-
-    # --------
-    # overloaded functions
-    # --------
-
-    def configure_for_initialise(self):
-        """user code here. is called when the state is set to STANDBY"""
-
-        # make sure all sockets are named
-        if len(self.PSOC_sockets) != self.PSOC_SOCKETS:
-            raise Exception(
-                f"At least {self.PSOC_NOF_SOCKETS} names are required to be given. \
-                You can simply leave any unused sockets as empty strings"
-            )
-        else:
-            # create a dict with the name of the sockets being keys for the socket number
-            # (e.g: "socket_nr_1": 1)
-            self.socket_dict = {
-                self.PSOC_sockets[f]: f + 1 for f in range(len(self.PSOC_sockets))
-            }
-            logger.debug(
-                "Configured PSOC with the following socket names: %s", self.PSOC_sockets
-            )
-
-        super().configure_for_initialise(simulator_class=PSOC_sim)
-
-    def _toggle_socket(self, socket_name, _on: bool):
-        """
-        This function is tailored to the "APS switched rack PSOC",
-        changing the psoc will require some changes to this function
-        """
-
-        try:
-            socket_nr = self.socket_dict[socket_name]
-        except Exception as exc:
-            raise Exception(
-                (
-                    f"This is not a valid socket name, please make sure "
-                    f"it is one of the following: {self.socket_dict.keys()}"
-                )
-            ) from exc
-
-        # get the correct value to set
-        if _on:
-            socket_set = "outletOn"
-        else:
-            socket_set = "outletOff"
-
-        # create the SNMPAttribute for the correct socket
-        attr = self.SNMP_attribute_class(
-            self.snmp_manager.SNMP_comm,
-            "PowerNet-MIB",
-            name="sPDUOutletCtl",
-            idx=socket_nr,
-            dtype=str,
-            dim_x=1,
-            dim_y=0,
-        )
-
-        # write the correct value
-        attr.write_function([socket_set])
-
-    def _power_hardware_on(self):
-        self.power_ccd_on()
-
-    def _power_hardware_off(self):
-        self.power_ccd_off()
-
-    # --------
-    # Commands
-    # --------
-
-    @command(dtype_in=str)
-    def socket_on(self, socket_name):
-        """Turn socket on"""
-        self._toggle_socket(socket_name, _on=True)
-        logger.debug("Turned socket %s on", {socket_name})
-
-    @command(dtype_in=str)
-    def socket_off(self, socket_name):
-        """Turn socket off"""
-        self._toggle_socket(socket_name, _on=False)
-        logger.debug("Turned socket %s off", {socket_name})
-
-    @command(dtype_out=str)
-    def readable_uptime(self):
-        """
-        This function returns a readable string of the uptime.
-        """
-        # prepares this object for the readable_uptime command
-        uptime_attr = self.SNMP_attribute_class(
-            self.snmp_manager.SNMP_comm,
-            "SNMPv2-MIB",
-            name="sysUpTime",
-            idx=0,
-            dtype=numpy.int64,
-            dim_x=1,
-            dim_y=0,
-            scaling_factor=0.01,
-        )
-        # for whatever reason, the uptime is given in hundredts of a second
-        return str(timedelta(seconds=uptime_attr.read_function()))
-
-    @command()
-    @only_in_states(DEFAULT_COMMAND_STATES)
-    def power_ccd_on(self):
-        """Turn on 230V to the CCD"""
-        self.socket_on("socket_1")
-
-    @command()
-    @only_in_states(DEFAULT_COMMAND_STATES)
-    def power_ccd_off(self):
-        """Turn off 230V to the CCD"""
-        self.socket_off("socket_1")
diff --git a/tangostationcontrol/tangostationcontrol/devices/types.py b/tangostationcontrol/tangostationcontrol/devices/types.py
index 01216170e..43c4d67bb 100644
--- a/tangostationcontrol/tangostationcontrol/devices/types.py
+++ b/tangostationcontrol/tangostationcontrol/devices/types.py
@@ -27,7 +27,6 @@ class DeviceTypes(str, Enum):
     ObservationControl = "ObservationControl"
     Metadata = "Metadata"
     PCON = "PCON"
-    PSOC = "PSOC"
     RECV = "RECV"
     RECVH = "RECVH"
     RECVL = "RECVL"
diff --git a/tangostationcontrol/tangostationcontrol/test/dummy_observation_settings.py b/tangostationcontrol/tangostationcontrol/test/dummy_observation_settings.py
index b0b1a2f29..8b8d89634 100644
--- a/tangostationcontrol/tangostationcontrol/test/dummy_observation_settings.py
+++ b/tangostationcontrol/tangostationcontrol/test/dummy_observation_settings.py
@@ -13,7 +13,7 @@ from tangostationcontrol.configuration import (
     XST,
 )
 
-SETTINGS_TWO_FIELDS = ObservationSettings(
+SETTINGS_TWO_FIELDS_CORE = ObservationSettings(
     "cs001",
     [
         ObservationFieldSettings(
@@ -43,7 +43,7 @@ SETTINGS_TWO_FIELDS = ObservationSettings(
     ],
 )
 
-SETTINGS_HBA_IMMEDIATE = ObservationSettings(
+SETTINGS_HBA_CORE_IMMEDIATE = ObservationSettings(
     "cs001",
     [
         ObservationFieldSettings(
@@ -71,11 +71,11 @@ SETTINGS_HBA_IMMEDIATE = ObservationSettings(
 )
 
 
-def get_observation_settings_two_fields() -> ObservationSettings:
+def get_observation_settings_two_fields_core() -> ObservationSettings:
     """Get an observation with two antenna fields"""
-    return copy.deepcopy(SETTINGS_TWO_FIELDS)
+    return copy.deepcopy(SETTINGS_TWO_FIELDS_CORE)
 
 
-def get_observation_settings_hba_immediate() -> ObservationSettings:
+def get_observation_settings_hba_core_immediate() -> ObservationSettings:
     """Get an observation with one antenna field and no start time"""
-    return copy.deepcopy(SETTINGS_HBA_IMMEDIATE)
+    return copy.deepcopy(SETTINGS_HBA_CORE_IMMEDIATE)
diff --git a/tangostationcontrol/test/clients/test_snmp_client.py b/tangostationcontrol/test/clients/test_snmp_client.py
index 3db587e7f..3812ffa46 100644
--- a/tangostationcontrol/test/clients/test_snmp_client.py
+++ b/tangostationcontrol/test/clients/test_snmp_client.py
@@ -16,7 +16,6 @@ from tangostationcontrol.clients.snmp.snmp_client import SNMPComm
 from tangostationcontrol.clients.snmp.attribute_classes import (
     SNMPAttribute,
     PCON_sim,
-    PSOC_sim,
 )
 
 
@@ -328,42 +327,6 @@ class TestSNMPSimulators(base.TestCase):
             pwr_status.read_function(), 1, "Could not read single value attribute"
         )
 
-        # read socket 5 out of the socket list
-        socket_5 = PSOC_sim(name="sPDUOutletCtl", idx=5, dim_x=1, dim_y=0)
-        self.assertEqual(
-            socket_5.read_function(),
-            "outletOn",
-            "Could not read single value out of array",
-        )
-
-        # read all sockets at once
-        all_sockets = PSOC_sim(name="sPDUOutletCtl", idx=1, dim_x=8, dim_y=0)
-        all_socket_expected = 8 * ["outletOn"]
-        self.assertListEqual(
-            all_sockets.read_function(),
-            all_socket_expected,
-            "Could not read entire array at once",
-        )
-
-        # set socket 5 to off
-        socket_5.write_function("outletOff")
-        self.assertEqual(
-            socket_5.read_function(),
-            "outletOff",
-            "could not write value to sPDUOutletCtl",
-        )
-
-        # also check the full socket list
-        all_socket_expected[4] = "outletOff"
-        self.assertListEqual(
-            all_sockets.read_function(), all_socket_expected, "list updated incorrectly"
-        )
-
-        # test reading an attribute that does not exist
-        fake_name = PSOC_sim(name="fake_name", idx=1, dim_x=1, dim_y=0)
-        with self.assertRaises(NameError):
-            _ = fake_name.read_function()
-
 
 class TestMibLoading(base.TestCase):
     """Test class for MIB file load operations"""
diff --git a/tangostationcontrol/test/devices/base_device_classes/test_hierarchy_device.py b/tangostationcontrol/test/devices/base_device_classes/test_hierarchy_device.py
index 139837bdc..1db28e067 100644
--- a/tangostationcontrol/test/devices/base_device_classes/test_hierarchy_device.py
+++ b/tangostationcontrol/test/devices/base_device_classes/test_hierarchy_device.py
@@ -286,7 +286,7 @@ class TestHierarchyDevice(device_base.DeviceTestCase):
                 "stat/notexist/*", hierarchy_device.HierarchyMatchingFilter.REGEX
             )
 
-    TEST_CHILDREN_ROOT = ["stat/ccd/1", "stat/psoc/1", "stat/afl/lba"]
+    TEST_CHILDREN_ROOT = ["stat/ccd/1", "stat/afl/lba"]
 
     TEST_CHILDREN_ANTENNAFIELD = [
         "stat/sdp/1",
@@ -306,7 +306,6 @@ class TestHierarchyDevice(device_base.DeviceTestCase):
     # Calls to get_property follow depth-first order
     TEST_GET_PROPERTY_CALLS = [
         {TEST_PROPERTY_NAME: []},  # cdd/1
-        {TEST_PROPERTY_NAME: []},  # psoc/1
         {TEST_PROPERTY_NAME: TEST_CHILDREN_ANTENNAFIELD},  # afl/1
         {TEST_PROPERTY_NAME: TEST_CHILDREN_SDP},  # sdp/1
         {TEST_PROPERTY_NAME: []},  # xst/1
@@ -329,17 +328,12 @@ class TestHierarchyDevice(device_base.DeviceTestCase):
         def test_fn(test_hierarchy: TestHierarchyDevice.ConcreteHierarchy):
             test = test_hierarchy.children(depth=-1)
 
-            # Test ccd and psoc have no children
+            # Test ccd has no children
             self.assertEqual(
                 0,
                 len(test["stat/ccd/1"]["children"]),
                 msg=f'{test["stat/ccd/1"]["children"]}',
             )
-            self.assertEqual(
-                0,
-                len(test["stat/psoc/1"]["children"]),
-                msg=f'{test["stat/psoc/1"]["children"]}',
-            )
 
             # Test antennafield has 4 children
             self.assertEqual(4, len(test["stat/afl/lba"]["children"]))
diff --git a/tangostationcontrol/test/devices/test_psoc_device.py b/tangostationcontrol/test/devices/test_psoc_device.py
deleted file mode 100644
index aa09074a4..000000000
--- a/tangostationcontrol/test/devices/test_psoc_device.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
-
-from test.devices import device_base
-
-
-class TestPSOCDevice(device_base.DeviceTestCase):
-    # some dummy values for mandatory properties
-
-    psoc_properties = {
-        "SNMP_community": "public",
-        "SNMP_host": "10.87.2.145",
-        "SNMP_version": 1,
-        "SNMP_mib_dir": "devices/psoc_mib/PowerNet-MIB.mib",
-        "SNMP_timeout": 5.0,
-        "PSOC_sockets": [
-            "socket_1",
-            "socket_2",
-            "socket_3",
-            "socket_4",
-            "socket_5",
-            "socket_6",
-            "socket_7",
-            "socket_8",
-        ],
-    }
-
-    def setUp(self):
-        # DeviceTestCase setUp patches lofar_device DeviceProxy
-        super(TestPSOCDevice, self).setUp()
diff --git a/tangostationcontrol/test/observation/test_observation.py b/tangostationcontrol/test/observation/test_observation.py
index 748913ea1..7ada08ce2 100644
--- a/tangostationcontrol/test/observation/test_observation.py
+++ b/tangostationcontrol/test/observation/test_observation.py
@@ -9,8 +9,8 @@ from tango import DevState
 from tangostationcontrol.observation.observation import Observation
 from tangostationcontrol.observation import observation_field
 from tangostationcontrol.test.dummy_observation_settings import (
-    get_observation_settings_two_fields,
-    get_observation_settings_hba_immediate,
+    get_observation_settings_two_fields_core,
+    get_observation_settings_hba_core_immediate,
 )
 
 from test import base
@@ -19,7 +19,7 @@ from test import base
 @mock.patch("tango.Util.instance")
 class TestObservation(base.TestCase):
     def test_properties(self, _):
-        sut = Observation("DMR", get_observation_settings_two_fields())
+        sut = Observation("DMR", get_observation_settings_two_fields_core())
         self.assertEqual(5, sut.observation_id)
         self.assertEqual(["HBA0", "LBA"], sut.antenna_fields)
 
@@ -27,7 +27,7 @@ class TestObservation(base.TestCase):
     def mocked_observation_field_base(m_obs_field) -> Observation:
         """Base function for tests using mocked ObservationField instances"""
 
-        obs_settings = get_observation_settings_two_fields()
+        obs_settings = get_observation_settings_two_fields_core()
         sut = Observation("DMR", obs_settings)
 
         for antenna_field in obs_settings.antenna_fields:
@@ -67,11 +67,11 @@ class TestObservation(base.TestCase):
         self.assertTrue(sut.is_partially_running())
 
     def test_create_devices(self, tu_mock):
-        sut = Observation("DMR", get_observation_settings_two_fields())
+        sut = Observation("DMR", get_observation_settings_two_fields_core())
         sut.create_devices()
 
         self.assertEqual(
-            len(get_observation_settings_two_fields().antenna_fields),
+            len(get_observation_settings_two_fields_core().antenna_fields),
             len(sut._observation_fields),
         )
 
@@ -102,7 +102,7 @@ class TestObservation(base.TestCase):
             obs_field.start.assert_called_once()
 
     def test_update_observation_state(self, tu_mock):
-        sut = Observation("DMR", get_observation_settings_hba_immediate())
+        sut = Observation("DMR", get_observation_settings_hba_core_immediate())
 
         sut._stop_antenna_field = mock.Mock()
         sut._start_antenna_field = mock.Mock()
@@ -128,7 +128,7 @@ class TestObservation(base.TestCase):
 
     def test_observation_callback(self, tu_mock):
         """Test the start / stop observation callbacks"""
-        t_params = get_observation_settings_hba_immediate()
+        t_params = get_observation_settings_hba_core_immediate()
 
         t_callback = mock.Mock()
 
@@ -163,7 +163,9 @@ class TestObservation(base.TestCase):
     def bound_callback(self, obs_id: int):
         """Test passing bound (self) callback"""
         self.assertEqual(
-            get_observation_settings_hba_immediate().antenna_fields[0].observation_id,
+            get_observation_settings_hba_core_immediate()
+            .antenna_fields[0]
+            .observation_id,
             obs_id,
         )
 
@@ -171,7 +173,7 @@ class TestObservation(base.TestCase):
         """Test calling bound callback"""
         sut = Observation(
             tango_domain="DMR",
-            parameters=get_observation_settings_hba_immediate(),
+            parameters=get_observation_settings_hba_core_immediate(),
             start_callback=self.bound_callback,
         )
         sut._stop_antenna_field = mock.Mock()
diff --git a/tangostationcontrol/test/observation/test_observation_controller.py b/tangostationcontrol/test/observation/test_observation_controller.py
index 6381f588a..e7178393f 100644
--- a/tangostationcontrol/test/observation/test_observation_controller.py
+++ b/tangostationcontrol/test/observation/test_observation_controller.py
@@ -10,7 +10,7 @@ from unittest import mock
 from tango import DevFailed
 from tangostationcontrol.observation import observation_controller as obs_module
 from tangostationcontrol.test.dummy_observation_settings import (
-    get_observation_settings_two_fields,
+    get_observation_settings_two_fields_core,
 )
 
 from test import base
@@ -126,7 +126,7 @@ class TestObservationController(base.TestCase):
         self.assertListEqual(["LBA"], sut.active_antenna_fields)
 
     def test_add_observation(self, _m_tango_util):
-        settings = get_observation_settings_two_fields()
+        settings = get_observation_settings_two_fields_core()
         for antenna_field in settings.antenna_fields:
             antenna_field.stop_time = (datetime.now() + timedelta(days=1)).isoformat()
 
@@ -154,7 +154,7 @@ class TestObservationController(base.TestCase):
     def test_stop_callback(self, _m_tango_util):
         """Test that the _stop_callback correctly cleans up observations"""
 
-        settings = get_observation_settings_two_fields()
+        settings = get_observation_settings_two_fields_core()
         for antenna_field in settings.antenna_fields:
             antenna_field.stop_time = (datetime.now() + timedelta(days=1)).isoformat()
 
@@ -170,7 +170,7 @@ class TestObservationController(base.TestCase):
         self.assertEqual(0, len(sut))
 
     def test_add_observation_failed(self, _m_tango_util):
-        settings = get_observation_settings_two_fields()
+        settings = get_observation_settings_two_fields_core()
         for antenna_field in settings.antenna_fields:
             antenna_field.stop_time = (datetime.now() + timedelta(days=1)).isoformat()
 
diff --git a/tangostationcontrol/test/observation/test_observation_field.py b/tangostationcontrol/test/observation/test_observation_field.py
index a18685765..0cb841391 100644
--- a/tangostationcontrol/test/observation/test_observation_field.py
+++ b/tangostationcontrol/test/observation/test_observation_field.py
@@ -11,7 +11,7 @@ from tango import DevState, DevFailed
 from tangostationcontrol.observation import observation_field
 from tangostationcontrol.common.proxies.proxy import create_device_proxy
 from tangostationcontrol.test.dummy_observation_settings import (
-    get_observation_settings_two_fields,
+    get_observation_settings_two_fields_core,
 )
 
 from test import base
@@ -21,7 +21,7 @@ from test import base
 class TestObservationField(base.TestCase):
     def test_properties(self, _):
         sut = observation_field.ObservationField(
-            "DMR", get_observation_settings_two_fields().antenna_fields[0]
+            "DMR", get_observation_settings_two_fields_core().antenna_fields[0]
         )
         self.assertEqual(5, sut.observation_id)
         self.assertEqual("HBA0", sut.antenna_field)
@@ -30,7 +30,7 @@ class TestObservationField(base.TestCase):
 
     def test_create_observation_device(self, tu_mock):
         sut = observation_field.ObservationField(
-            "DMR", get_observation_settings_two_fields().antenna_fields[0]
+            "DMR", get_observation_settings_two_fields_core().antenna_fields[0]
         )
         sut.create_observation_field_device()
 
@@ -41,7 +41,7 @@ class TestObservationField(base.TestCase):
     def test_create_observation_device_fail(self, tu_mock):
         """Test creation failed"""
         sut = observation_field.ObservationField(
-            "DMR", get_observation_settings_two_fields().antenna_fields[0]
+            "DMR", get_observation_settings_two_fields_core().antenna_fields[0]
         )
 
         with mock.patch.object(observation_field, "logger") as m_logger:
@@ -55,13 +55,13 @@ class TestObservationField(base.TestCase):
     def test_initialise_observation_field(self, dp_mock, tu_mock):
         importlib.reload(sys.modules[create_device_proxy.__module__])
         sut = observation_field.ObservationField(
-            "DMR", get_observation_settings_two_fields().antenna_fields[0]
+            "DMR", get_observation_settings_two_fields_core().antenna_fields[0]
         )
         sut.initialise_observation_field()
 
         self.assertEqual(
             dp_mock.return_value.observation_field_settings_RW,
-            get_observation_settings_two_fields().antenna_fields[0].to_json(),
+            get_observation_settings_two_fields_core().antenna_fields[0].to_json(),
         )
         dp_mock.return_value.Initialise.assert_called()
 
@@ -69,7 +69,7 @@ class TestObservationField(base.TestCase):
     def test_start(self, dp_mock, tu_mock):
         importlib.reload(sys.modules[create_device_proxy.__module__])
         sut = observation_field.ObservationField(
-            "DMR", get_observation_settings_two_fields().antenna_fields[0]
+            "DMR", get_observation_settings_two_fields_core().antenna_fields[0]
         )
         sut.initialise_observation_field()
         sut.start()
@@ -79,7 +79,7 @@ class TestObservationField(base.TestCase):
     def test_stop(self, tu_mock):
         importlib.reload(sys.modules[observation_field.ObservationField.__module__])
         sut = observation_field.ObservationField(
-            "DMR", get_observation_settings_two_fields().antenna_fields[0]
+            "DMR", get_observation_settings_two_fields_core().antenna_fields[0]
         )
 
         dp_mock = Mock()
-- 
GitLab