diff --git a/README.md b/README.md
index d6189af1a9b672d9beb366d275d224d0ea8b9b43..e1914fa754a553a44b2622de139379520a944381 100644
--- a/README.md
+++ b/README.md
@@ -150,6 +150,7 @@ Next change the version in the following places:
    through [https://git.astron.nl/lofar2.0/tango/-/tags](Deploy Tags)
 
 # Release Notes
+* 0.47.1 Move GrafanaAPIV3 RPC interface to Opah repo
 * 0.47.0 Migrate from lofar-station-client to lofar-lotus package. Update various package dependencies.
 * 0.46.1 Include clock_RW in metadata JSON
 * 0.46.0 Expose latest BST/SST/XST from ZMQ over gRPC
diff --git a/tangostationcontrol/VERSION b/tangostationcontrol/VERSION
index 421ab545d9adc503fddf2ea2ce447a9a8c5323af..46e33ab94c3e95d77ab81dee1693be27bde0a2d4 100644
--- a/tangostationcontrol/VERSION
+++ b/tangostationcontrol/VERSION
@@ -1 +1 @@
-0.47.0
+0.47.1
\ No newline at end of file
diff --git a/tangostationcontrol/proto/README.md b/tangostationcontrol/proto/README.md
deleted file mode 100644
index 42f5d292ac169bd47ab64000d8c355c3bc96a3f4..0000000000000000000000000000000000000000
--- a/tangostationcontrol/proto/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-Protobuf definitions for the RPC services in the tangostationcontrol.rpc module.
-
-`grafana-apiv3.proto` is taken from https://raw.githubusercontent.com/innius/grafana-simple-grpc-datasource/master/pkg/proto/v3/apiv3.proto
diff --git a/tangostationcontrol/proto/antennafield.proto b/tangostationcontrol/proto/antennafield.proto
deleted file mode 100644
index c4f92628e48c51812dbe2d968612576903015228..0000000000000000000000000000000000000000
--- a/tangostationcontrol/proto/antennafield.proto
+++ /dev/null
@@ -1,57 +0,0 @@
-syntax = "proto3";
-
-service Antennafield {
-  rpc GetAntenna(GetAntennaRequest) returns (AntennaReply) {}
-  rpc SetAntennaStatus(SetAntennaStatusRequest) returns (AntennaReply) {}
-  rpc SetAntennaUse(SetAntennaUseRequest) returns (AntennaReply) {}
-}
-
-enum Antenna_Status {
-  OK = 0;
-  SUSPICIOUS = 1;
-  BROKEN = 2;
-  BEYOND_REPAIR = 3;
-  NOT_AVAILABLE = 4;
-}
-
-enum Antenna_Use {
-  // use antenna only if it's OK or SUSPICIOUS
-  AUTO = 0;
-  // force antenna to be on, regardless of status
-  ON = 1;
-  // force antenna to be off, regardless of status
-  OFF = 2;
-}
-
-message Identifier {
-  // e.g. "LBA"
-  string antennafield_name = 1;
-  // e.g. "LBA00"
-  string antenna_name = 2;
-}
-
-message SetAntennaStatusRequest {
-  Identifier identifier = 1;
-  Antenna_Status antenna_status = 2 ;
-}
-
-message GetAntennaRequest {
-  Identifier identifier = 1;
-}
-
-message SetAntennaUseRequest {
-  Identifier identifier = 1;
-  Antenna_Use antenna_use = 2;
-}
-
-message AntennaResult {
-  Identifier identifier = 1;
-  Antenna_Use antenna_use = 2;
-  Antenna_Status antenna_status = 3;
-}
-
-message AntennaReply {
-  bool success = 1;
-  string exception = 2;
-  AntennaResult result = 3;
-}
diff --git a/tangostationcontrol/proto/grafana-apiv3.proto b/tangostationcontrol/proto/grafana-apiv3.proto
deleted file mode 100644
index 01a960b09e990cee7881c7a5f22576745f9f75fb..0000000000000000000000000000000000000000
--- a/tangostationcontrol/proto/grafana-apiv3.proto
+++ /dev/null
@@ -1,360 +0,0 @@
-syntax = "proto3";
-
-option go_package = "bitbucket.org/innius/grafana-simple-grpc-datasource/v3";
-
-import "google/protobuf/timestamp.proto";
-
-package grafanav3;
-
-// The GrafanaQueryAPI definition.
-service GrafanaQueryAPI {
-  // Returns a list of all available dimensions
-  rpc ListDimensionKeys (ListDimensionKeysRequest) returns (ListDimensionKeysResponse) {
-  }
-
-  // Returns a list of all dimension values for a certain dimension
-  rpc ListDimensionValues (ListDimensionValuesRequest) returns (ListDimensionValuesResponse) {
-  }
-
-  // Returns all metrics from the system
-  rpc ListMetrics (ListMetricsRequest) returns (ListMetricsResponse) {
-  }
-
-  // Gets the options for the specified query type
-  rpc GetQueryOptions (GetOptionsRequest) returns (GetOptionsResponse) {
-
-  }
-
-  // Gets the last known value for one or more metrics
-  rpc GetMetricValue (GetMetricValueRequest) returns (GetMetricValueResponse) {
-  }
-
-  // Gets the history for one or more metrics
-  rpc GetMetricHistory (GetMetricHistoryRequest) returns (GetMetricHistoryResponse) {
-  }
-
-  // Gets the history for one or more metrics
-  rpc GetMetricAggregate(GetMetricAggregateRequest) returns (GetMetricAggregateResponse) {
-  }
-}
-
-message ListMetricsRequest {
-  repeated Dimension dimensions = 1;
-  string filter = 2;
-}
-
-message ListMetricsResponse {
-  message Metric {
-    string name = 1;
-    string description = 2;
-  }
-  repeated Metric Metrics = 1;
-}
-
-message GetMetricValueRequest {
-  repeated Dimension dimensions = 1;
-
-  repeated string metrics = 2;
-
-  map<string,string> options = 3 ;
-
-  google.protobuf.Timestamp startDate = 4;
-  google.protobuf.Timestamp endDate = 5;
-}
-
-message GetMetricValueResponse {
-  message Frame {
-    string metric = 1;
-
-    google.protobuf.Timestamp timestamp = 2;
-
-    repeated SingleValueField fields = 3;
-
-    FrameMeta meta = 4;
-  }
-
-  repeated Frame frames = 1;
-}
-
-message GetOptionsRequest {
-  enum QueryType {
-    GetMetricHistory = 0;
-    GetMetricValue=1;
-    GetMetricAggregate=2;
-  }
-  // the query type for which options are requested
-  QueryType queryType = 1;
-
-  // the query options which are currently selected 
-  map<string,string> selectedOptions = 2 ;
-}
-
-message EnumValue {
-  // the id of the enum value 
-  string id = 1;
-  // the description of the option
-  string description = 2;
-  // the label of the option 
-  string label = 3;
-  // the default enum value
-  bool default = 4;
-}
-
-message Option {
-  // the id of the option 
-  string id = 1; 
-  string description = 2;
-  enum Type {
-    Enum = 0; // enum is rendered as a Select control in the frontend 
-    Boolean = 1;
-  }
-  Type  type = 3; 
-  repeated EnumValue enumValues = 4;
-  bool required = 5; 
-  // the label of the option 
-  string label = 6;
-}
-
-message GetOptionsResponse {
-  repeated Option options = 1;
-}
-
-message GetMetricAggregateRequest {
-  // The dimensions for the query
-  repeated Dimension dimensions = 1;
-
-  // the metrics for which the aggregates are retrieved
-  repeated string metrics = 2;
-
-  google.protobuf.Timestamp startDate = 4;
-  google.protobuf.Timestamp endDate = 5;
-  int64 maxItems = 6;
-  TimeOrdering timeOrdering = 7;
-  string startingToken = 8;
-  int64 intervalMs = 9;
-  map<string,string> options = 10 ;
-}
-
-message GetMetricAggregateResponse {
-  repeated Frame frames = 1;
-
-  string nextToken = 2;
-}
-
-message GetMetricHistoryRequest {
-  repeated Dimension dimensions = 3;
-  repeated string metrics = 4;
-  google.protobuf.Timestamp startDate = 5;
-  google.protobuf.Timestamp endDate = 6;
-  int64 maxItems = 7;
-  TimeOrdering timeOrdering = 8;
-  string startingToken = 9;
-  map<string,string> options = 10 ;
-}
-
-message GetMetricHistoryResponse {
-  repeated Frame frames = 1;
-
-  string nextToken = 2;
-}
-
-message Label {
-  string key = 1;
-  string value = 2;
-}
-
-message Field {
-  string name = 1;
-
-  repeated Label labels = 2;
-
-  config config = 3;
-
-  repeated double values = 4;
-  repeated string stringValues = 5;
-}
-
-message ValueMapping {
-  double from = 1;
-  double to = 2; 
-  string value = 3; 
-  string text = 4;
-  string color = 5;
-}
-
-message config {
-  string unit = 1;
-
-  repeated ValueMapping Mappings = 2;
-}
-
-message SingleValueField {
-  string name = 1;
-
-  repeated Label labels = 2;
-
-  config config = 3;
-
-  double value = 4;
-
-  string stringValue = 5;
-}
-
-// The data frame for each metric
-message Frame {
-  string metric = 1;
-
-  repeated google.protobuf.Timestamp timestamps = 2;
-
-  repeated Field fields = 3;
-
-  FrameMeta meta = 4;
-}
-
-// FrameMeta matches:
-// https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L11
-// NOTE -- in javascript this can accept any `[key: string]: any;` however
-// this interface only exposes the values we want to be exposed
-message FrameMeta {
-  enum FrameType {
-    FrameTypeUnknown = 0;
-    FrameTypeTimeSeriesWide = 1;
-    FrameTypeTimeSeriesLong = 2;
-    FrameTypeTimeSeriesMany = 3;
-    FrameTypeDirectoryListing = 4;
-    FrameTypeTable = 5;
-  }
-  // Type asserts that the frame matches a known type structure
-  FrameType type = 1 ;
-
-  message Notice {
-    enum NoticeSeverity {
-      // NoticeSeverityInfo is informational severity.
-      NoticeSeverityInfo = 0;
-      // NoticeSeverityWarning is warning severity.
-      NoticeSeverityWarning = 1;
-      // NoticeSeverityError is error severity.
-      NoticeSeverityError = 3;
-    }
-    // Severity is the severity level of the notice: info, warning, or error.
-    NoticeSeverity Severity = 1;
-
-    // Text is freeform descriptive text for the notice.
-    string text = 2;
-
-    // Link is an optional link for display in the user interface and can be an
-    // absolute URL or a path relative to Grafana's root url.
-    string link = 3;
-
-    enum InspectType {
-      // InspectTypeNone is no suggestion for a tab of the panel editor in Grafana's user interface.
-      InspectTypeNone = 0;
-
-      // InspectTypeMeta suggests the "meta" tab of the panel editor in Grafana's user interface.
-      InspectTypeMeta = 1;
-
-      // InspectTypeError suggests the "error" tab of the panel editor in Grafana's user interface.
-      InspectTypeError = 2;
-
-      // InspectTypeData suggests the "data" tab of the panel editor in Grafana's user interface.
-      InspectTypeData = 3;
-
-      // InspectTypeStats suggests the "stats" tab of the panel editor in Grafana's user interface.
-      InspectTypeStats = 4;
-    }
-    // Inspect is an optional suggestion for which tab to display in the panel inspector
-    // in Grafana's User interface. Can be meta, error, data, or stats.
-    InspectType inspect = 4;
-  }
-  // Notices provide additional information about the data in the Frame that
-  // Grafana can display to the user in the user interface.
-  repeated Notice  Notices = 6;
-
-  // VisType is used to indicate how the data should be visualized in explore.
-  enum VisType {
-    // VisTypeGraph indicates the response should be visualized using a graph.
-    VisTypeGraph = 0;
-
-    // VisTypeTable indicates the response should be visualized using a table.
-    VisTypeTable = 1;
-
-    // VisTypeLogs indicates the response should be visualized using a logs visualization.
-    VisTypeLogs = 2;
-
-    // VisTypeTrace indicates the response should be visualized using a trace view visualization.
-    VisTypeTrace = 3;
-
-    // VisTypeNodeGraph indicates the response should be visualized using a node graph visualization.
-    VisTypeNodeGraph = 4;
-  }
-
-  // PreferredVisualization is currently used to show results in Explore only in preferred visualisation option.
-  VisType PreferredVisualization = 8;
-
-  // ExecutedQueryString is the raw query sent to the underlying system. All macros and templating
-  // have been applied.  When metadata contains this value, it will be shown in the query inspector.
-  string executedQueryString = 9;
-}
-
-enum TimeOrdering {
-  ASCENDING = 0;
-  DESCENDING = 1;
-}
-
-message ListDimensionKeysRequest {
-  string filter = 1;
-  repeated Dimension selected_dimensions = 2;
-}
-
-message ListDimensionKeysResponse {
-  message Result {
-    string key = 1;
-    string description = 2;
-  }
-  repeated Result results = 1;
-}
-
-message ListDimensionValuesRequest {
-  string dimension_key = 1;
-  string filter = 2;
-  repeated Dimension selected_dimensions = 3;
-}
-
-message ListDimensionValuesResponse {
-  message Result {
-    string value = 1;
-    string description = 2;
-  }
-  repeated Result results = 1;
-}
-
-message TimeRange {
-  int64 fromEpochMS = 1;
-  int64 toEpochMS = 2;
-}
-
-message Dimension {
-  string key = 1;
-  string value = 2;
-}
-
-message QueryRequest {
-  string refId = 1;
-  int64 maxDataPoints = 2;
-  int64 intervalMS = 3;
-  TimeRange timeRange = 4;
-  // The offset for the result set
-  int64 startKey = 5;
-  repeated Dimension dimensions = 6;
-}
-
-// The response message containing the greetings
-message QueryResponse {
-  string refId = 1;
-  int64 nextKey = 2;
-  message Value {
-    int64 timestamp = 1;
-    float value = 2;
-  }
-  repeated Value values = 3;
-}
diff --git a/tangostationcontrol/proto/observation.proto b/tangostationcontrol/proto/observation.proto
deleted file mode 100644
index 3a08fe61c83d546fcda1012a641227ff7246e00d..0000000000000000000000000000000000000000
--- a/tangostationcontrol/proto/observation.proto
+++ /dev/null
@@ -1,18 +0,0 @@
-syntax = "proto3";
-
-service Observation {
-  rpc StartObservation(StartObservationRequest) returns (ObservationReply){}
-  rpc StopObservation(StopObservationRequest) returns (ObservationReply) {}
-}
-
-message StartObservationRequest {
-  string configuration = 1;
-}
-message StopObservationRequest {
-  int64 observation_id = 1;
-}
-
-message ObservationReply {
-  bool success = 1;
-  string exception = 2;
-}
diff --git a/tangostationcontrol/proto/statistics.proto b/tangostationcontrol/proto/statistics.proto
deleted file mode 100644
index 67d55f30ee17d7a5342765635a324ac425e84144..0000000000000000000000000000000000000000
--- a/tangostationcontrol/proto/statistics.proto
+++ /dev/null
@@ -1,101 +0,0 @@
-syntax = "proto3";
-
-import "google/protobuf/timestamp.proto";
-
-service Statistics {
-  rpc Bst(BstRequest) returns (BstReply) {}
-  rpc Sst(SstRequest) returns (SstReply) {}
-  rpc Xst(XstRequest) returns (XstReply) {}
-}
-
-message FrequencyBand {
-  string antenna_type = 1;
-  int32 clock = 2;
-  int32 nyquist_zone = 3;
-}
-
-message BstRequest {
-  string antenna_field = 1;
-  optional uint32 maxage = 2;
-}
-
-message BstResult {
-  google.protobuf.Timestamp timestamp = 1;
-  FrequencyBand frequency_band = 2;
-  float integration_interval = 3;
-
-  message BstBeamlet {
-    int32 beamlet = 1;
-    float x_power_db = 2;
-    float y_power_db = 3;
-  }
-
-  repeated BstBeamlet beamlets = 4;
-}
-
-message BstReply {
-  BstResult result = 3;
-}
-
-
-message SstRequest {
-  string antenna_field = 1;
-  optional uint32 maxage = 2;
-}
-
-message SstResult {
-  google.protobuf.Timestamp timestamp = 1;
-  FrequencyBand frequency_band = 2;
-  float integration_interval = 3;
-
-  message SstSubband {
-    int32 subband = 1;
-
-    message SstAntenna {
-      int32 antenna = 1;
-      float x_power_db = 2;
-      float y_power_db = 3;
-    }
-
-    repeated SstAntenna antennas = 2;
-  }
-
-  repeated SstSubband subbands = 4;
-}
-
-message SstReply {
-  SstResult result = 3;
-}
-
-message XstRequest {
-  string antenna_field = 1;
-  optional uint32 maxage = 2;
-}
-
-message XstResult {
-  google.protobuf.Timestamp timestamp = 1;
-  FrequencyBand frequency_band = 2;
-  float integration_interval = 3;
-  int32 subband = 4;
-
-  message XstBaseline {
-    int32 antenna1 = 1;
-    int32 antenna2 = 2;
-
-    message XstValue {
-      float power_db = 1;
-      float phase = 2;
-    }
-
-    XstValue xx = 3;
-    XstValue xy = 4;
-    XstValue yx = 5;
-    XstValue yy = 6;
-  }
-
-  repeated XstBaseline baselines = 5;
-}
-
-message XstReply {
-  XstResult result = 3;
-}
diff --git a/tangostationcontrol/tangostationcontrol/rpc/antennafield.py b/tangostationcontrol/tangostationcontrol/rpc/antennafield.py
index b90731ae88e32b99d87aff32a721ce9e2132bc89..a5a88e7dbba100afee30e12566c7446b2d6dd62a 100644
--- a/tangostationcontrol/tangostationcontrol/rpc/antennafield.py
+++ b/tangostationcontrol/tangostationcontrol/rpc/antennafield.py
@@ -6,9 +6,9 @@ import tango
 from tango import DeviceProxy
 
 from tangostationcontrol.common.antennas import antenna_field_family_name
-from tangostationcontrol.rpc._proto import antennafield_pb2_grpc
+from lofar_sid.interface.stationcontrol import antennafield_pb2_grpc
 
-from tangostationcontrol.rpc._proto.antennafield_pb2 import (
+from lofar_sid.interface.stationcontrol.antennafield_pb2 import (
     AntennaReply,
     AntennaResult,
     GetAntennaRequest,
diff --git a/tangostationcontrol/tangostationcontrol/rpc/grafana_api.py b/tangostationcontrol/tangostationcontrol/rpc/grafana_api.py
deleted file mode 100644
index b3e66eec6ca6e5eeaaa82b45aca9cc7127e16f58..0000000000000000000000000000000000000000
--- a/tangostationcontrol/tangostationcontrol/rpc/grafana_api.py
+++ /dev/null
@@ -1,525 +0,0 @@
-# Copyright (C) 2025 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
-
-"""Exposure of the station's statistics for the innius-rpc-datasource plugin in Grafana."""
-
-from datetime import datetime, timezone
-import itertools
-import math
-from typing import Callable
-
-from tangostationcontrol.common.frequency_bands import Band, bands
-from tangostationcontrol.rpc._proto import grafana_apiv3_pb2
-from tangostationcontrol.rpc._proto import grafana_apiv3_pb2_grpc
-from tangostationcontrol.rpc._proto import statistics_pb2
-from tangostationcontrol.rpc.statistics import (
-    Statistics,
-    TooOldError,
-)
-from tangostationcontrol.rpc.common import (
-    call_exception_metrics,
-)
-
-
-class StatisticsToGrafana:
-    """Abstract base class for converting Statistics responses to Grafana Frames."""
-
-    # Meta information for our statistics
-    meta = grafana_apiv3_pb2.FrameMeta(
-        type=grafana_apiv3_pb2.FrameMeta.FrameType.FrameTypeTimeSeriesLong,
-        PreferredVisualization=grafana_apiv3_pb2.FrameMeta.VisType.VisTypeGraph,
-    )
-
-    def __init__(self, statistics: Statistics):
-        super().__init__()
-
-        self.statistics = statistics
-
-    @staticmethod
-    def _subband_frequency_func(
-        frequency_band: statistics_pb2.FrequencyBand,
-    ) -> Callable[[int], float]:
-        """Return a function that converts a subband number into its central frequency (in Hz).
-
-        NB: Spectral inversion is assumed to have been configured correctly at time of recording.
-        """
-
-        band_name = Band.lookup_nyquist_zone(
-            frequency_band.antenna_type,
-            frequency_band.clock,
-            frequency_band.nyquist_zone,
-        )
-
-        return bands[band_name].subband_frequency
-
-    @staticmethod
-    def _verify_call_result(reply, time_window: tuple[datetime, datetime]):
-        """Verify reply from Statistics service. Raises if verification fails."""
-
-        if not (
-            time_window[0]
-            <= reply.result.timestamp.ToDatetime(tzinfo=timezone.utc)
-            < time_window[1]
-        ):
-            raise TooOldError(
-                f"Stastistics not available in time window {time_window}. Available is {reply.result.timestamp}."
-            )
-
-        return True
-
-
-class BstToGrafana(StatisticsToGrafana):
-    """Converts Statistics.BST responses to Grafana Frames."""
-
-    def _get_latest_in_window(
-        self, time_window: tuple[datetime, datetime], antenna_field: str
-    ) -> statistics_pb2.BstReply:
-        """Get the latest statistics in the given time window, if any."""
-
-        request = statistics_pb2.BstRequest(
-            antenna_field=antenna_field,
-        )
-        reply = self.statistics.Bst(request, None)
-
-        self._verify_call_result(reply, time_window)
-
-        return reply
-
-    @call_exception_metrics("StatisticsToGrafana", {"type": "bst"})
-    def all_frames(
-        self,
-        time_window: tuple[datetime, datetime],
-        antenna_field: str,
-        pol: str | None,
-    ) -> list[grafana_apiv3_pb2.Frame]:
-        """Return all Grafana Frames for the requested data."""
-
-        try:
-            reply = self._get_latest_in_window(time_window, antenna_field)
-            result = reply.result
-        except TooOldError:
-            return []
-
-        # Turn result into Grafana fields
-        #
-        # Each value describes the power for one beamlet.
-        #
-        # Each polarisation results in one field.
-        frames = [
-            grafana_apiv3_pb2.Frame(
-                metric="BST",
-                timestamps=[result.timestamp for _ in result.beamlets],
-                fields=list(
-                    filter(
-                        None,
-                        [
-                            grafana_apiv3_pb2.Field(
-                                name="beamlet",
-                                values=[b.beamlet for b in result.beamlets],
-                            ),
-                            (
-                                grafana_apiv3_pb2.Field(
-                                    name="xx",
-                                    config=grafana_apiv3_pb2.config(
-                                        unit="dB",
-                                    ),
-                                    values=[b.x_power_db for b in result.beamlets],
-                                )
-                                if pol in ["xx", None]
-                                else None
-                            ),
-                            (
-                                grafana_apiv3_pb2.Field(
-                                    name="yy",
-                                    config=grafana_apiv3_pb2.config(
-                                        unit="dB",
-                                    ),
-                                    values=[b.y_power_db for b in result.beamlets],
-                                )
-                                if pol in ["yy", None]
-                                else None
-                            ),
-                        ],
-                    )
-                ),
-                meta=self.meta,
-            )
-        ]
-
-        return frames
-
-
-class SstToGrafana(StatisticsToGrafana):
-    """Converts Statistics.SST responses to Grafana Frames."""
-
-    def _get_latest_in_window(
-        self, time_window: tuple[datetime, datetime], antenna_field: str
-    ) -> statistics_pb2.SstReply:
-        """Get the latest statistics in the given time window, if any."""
-
-        request = statistics_pb2.SstRequest(
-            antenna_field=antenna_field,
-        )
-        reply = self.statistics.Sst(request, None)
-
-        self._verify_call_result(reply, time_window)
-
-        return reply
-
-    @call_exception_metrics("StatisticsToGrafana", {"type": "sst"})
-    def all_frames(
-        self,
-        time_window: tuple[datetime, datetime],
-        antenna_field: str,
-        selected_pol: str | None,
-    ) -> list[grafana_apiv3_pb2.Frame]:
-        """Return all Grafana Frames for the requested data."""
-
-        try:
-            reply = self._get_latest_in_window(time_window, antenna_field)
-            result = reply.result
-        except TooOldError:
-            return []
-
-        # Turn result into Grafana fields
-        #
-        # Each value describes the power of an antenna for a specific subband.
-        #
-        # Field 0 are the spectral frequencies for each value.
-        # Field 1+ is one field for each antenna and each requested polarisation.
-        antenna_nrs = [antenna.antenna for antenna in result.subbands[0].antennas]
-        fields_per_antenna = [
-            [
-                (
-                    grafana_apiv3_pb2.Field(
-                        name="power",
-                        labels=[
-                            grafana_apiv3_pb2.Label(
-                                key="antenna",
-                                value="%03d" % antenna_nr,
-                            ),
-                            grafana_apiv3_pb2.Label(
-                                key="pol",
-                                value="xx",
-                            ),
-                        ],
-                        config=grafana_apiv3_pb2.config(
-                            unit="dB",
-                        ),
-                        values=[
-                            subband.antennas[antenna_nr].x_power_db
-                            for subband in result.subbands
-                        ],
-                    )
-                    if selected_pol in ["xx", None]
-                    else None
-                ),
-                (
-                    grafana_apiv3_pb2.Field(
-                        name="power",
-                        labels=[
-                            grafana_apiv3_pb2.Label(
-                                key="antenna",
-                                value="%03d" % antenna_nr,
-                            ),
-                            grafana_apiv3_pb2.Label(
-                                key="pol",
-                                value="yy",
-                            ),
-                        ],
-                        config=grafana_apiv3_pb2.config(
-                            unit="dB",
-                        ),
-                        values=[
-                            subband.antennas[antenna_nr].y_power_db
-                            for subband in result.subbands
-                        ],
-                    )
-                    if selected_pol in ["yy", None]
-                    else None
-                ),
-            ]
-            for antenna_nr in antenna_nrs
-        ]
-
-        subband_frequency = self._subband_frequency_func(result.frequency_band)
-
-        frames = [
-            grafana_apiv3_pb2.Frame(
-                metric="SST",
-                timestamps=[result.timestamp for _ in result.subbands],
-                fields=[
-                    grafana_apiv3_pb2.Field(
-                        name="frequency",
-                        config=grafana_apiv3_pb2.config(
-                            unit="Hz",
-                        ),
-                        values=[subband_frequency(b.subband) for b in result.subbands],
-                    ),
-                ]
-                + list(filter(None, itertools.chain(*fields_per_antenna))),
-                meta=self.meta,
-            )
-        ]
-
-        return frames
-
-
-class XstToGrafana(StatisticsToGrafana):
-    """Converts Statistics.XST responses to Grafana Frames."""
-
-    def _get_latest_in_window(
-        self, time_window: tuple[datetime, datetime], antenna_field: str
-    ) -> statistics_pb2.XstReply:
-        """Get the latest statistics in the given time window, if any."""
-
-        request = statistics_pb2.XstRequest(
-            antenna_field=antenna_field,
-        )
-        reply = self.statistics.Xst(request, None)
-
-        self._verify_call_result(reply, time_window)
-
-        return reply
-
-    @call_exception_metrics("StatisticsToGrafana", {"type": "xst"})
-    def all_frames(
-        self,
-        time_window: tuple[datetime, datetime],
-        antenna_field: str,
-        selected_pol: str | None,
-    ) -> list[grafana_apiv3_pb2.Frame]:
-        """Return all Grafana Frames for the requested data."""
-
-        try:
-            reply = self._get_latest_in_window(time_window, antenna_field)
-            result = reply.result
-        except TooOldError:
-            return []
-
-        subband_frequency = self._subband_frequency_func(result.frequency_band)
-
-        # Turn result into Grafana fields
-        #
-        # Each value describes a baseline.
-        #
-        # Field 0 & 1 are the (antenna1, antenna2) indices describing each baseline.
-        # Field 2 is the central frequency of the values for each baseline.
-        fields = [
-            grafana_apiv3_pb2.Field(
-                name="antenna1",
-                values=[baseline.antenna1 for baseline in result.baselines],
-            ),
-            grafana_apiv3_pb2.Field(
-                name="antenna2",
-                values=[baseline.antenna2 for baseline in result.baselines],
-            ),
-            grafana_apiv3_pb2.Field(
-                name="frequency",
-                config=grafana_apiv3_pb2.config(
-                    unit="Hz",
-                ),
-                values=[subband_frequency(result.subband) for _ in result.baselines],
-            ),
-        ]
-
-        # Subsequent fields describe the power and phase for each baseline, and
-        # the requested, or all, polarisations.
-        for pol in ("xx", "xy", "yx", "yy"):
-            if selected_pol is None or pol == selected_pol:
-                labels = (
-                    [
-                        grafana_apiv3_pb2.Label(
-                            key="pol",
-                            value=pol,
-                        )
-                    ]
-                    if not selected_pol
-                    else []
-                )
-
-                fields.extend(
-                    [
-                        grafana_apiv3_pb2.Field(
-                            name="power",
-                            labels=labels,
-                            config=grafana_apiv3_pb2.config(
-                                unit="dB",
-                            ),
-                            values=[
-                                getattr(baseline, pol).power_db
-                                for baseline in result.baselines
-                            ],
-                        ),
-                        grafana_apiv3_pb2.Field(
-                            name="phase",
-                            labels=labels,
-                            config=grafana_apiv3_pb2.config(
-                                unit="deg",
-                            ),
-                            values=[
-                                math.fabs(
-                                    getattr(baseline, pol).phase * 360.0 / math.pi
-                                )
-                                for baseline in result.baselines
-                            ],
-                        ),
-                    ]
-                )
-
-        frames = [
-            grafana_apiv3_pb2.Frame(
-                metric="XST",
-                timestamps=[result.timestamp for _ in result.baselines],
-                fields=fields,
-                meta=self.meta,
-            )
-        ]
-
-        return frames
-
-
-class GrafanaAPIV3(grafana_apiv3_pb2_grpc.GrafanaQueryAPIServicer):
-    """Implements the Grafana interface for the innius simple-rpc-datasource,
-    see https://github.com/innius/grafana-simple-grpc-datasource"""
-
-    def __init__(self, statistics: Statistics, antenna_fields: list[str]):
-        super().__init__()
-
-        self.statistics = statistics
-        self.antenna_fields = antenna_fields
-        self.bst = BstToGrafana(self.statistics)
-        self.sst = SstToGrafana(self.statistics)
-        self.xst = XstToGrafana(self.statistics)
-
-        # self._import_test_data()
-
-    def _import_test_data(self):
-        """Import test data for demo purposes."""
-
-        import json
-        import os
-
-        test_dir = os.path.dirname(__file__) + "/../../test/rpc/"
-
-        for type_ in ("bst", "sst", "xst"):
-            with open(test_dir + f"{type_}-message.json") as f:
-                print(f"Loading {f}")
-                message = json.load(f)
-
-                self.statistics.handle_statistics_message(
-                    f"{type_}/lba/cs032", datetime.now(tz=timezone.utc), message
-                )
-
-    def GetQueryOptions(self, request: grafana_apiv3_pb2.GetOptionsRequest, context):
-        """List options per query."""
-
-        return grafana_apiv3_pb2.GetOptionsResponse(options=[])
-
-    def ListDimensionKeys(
-        self, request: grafana_apiv3_pb2.ListDimensionKeysRequest, context
-    ):
-        """List available data dimensions."""
-
-        results = [
-            grafana_apiv3_pb2.ListDimensionKeysResponse.Result(
-                key="antenna_field",
-                description="Antenna field",
-            ),
-            grafana_apiv3_pb2.ListDimensionKeysResponse.Result(
-                key="pol",
-                description="Polarisation",
-            ),
-        ]
-        return grafana_apiv3_pb2.ListDimensionKeysResponse(results=results)
-
-    def ListDimensionValues(
-        self, request: grafana_apiv3_pb2.ListDimensionValuesRequest, context
-    ):
-        """List possible values for each data dimension."""
-
-        results = []
-
-        if request.dimension_key == "antenna_field":
-            for antenna_field in self.antenna_fields:
-                results.append(
-                    grafana_apiv3_pb2.ListDimensionValuesResponse.Result(
-                        value=antenna_field,
-                    )
-                )
-
-        if request.dimension_key == "pol":
-            for pol in ["xx", "xy", "yx", "yy"]:
-                results.append(
-                    grafana_apiv3_pb2.ListDimensionValuesResponse.Result(
-                        value=pol,
-                    )
-                )
-
-        return grafana_apiv3_pb2.ListDimensionValuesResponse(results=results)
-
-    def ListMetrics(self, request: grafana_apiv3_pb2.ListMetricsRequest, context):
-        """List available metrics."""
-
-        metrics = [
-            grafana_apiv3_pb2.ListMetricsResponse.Metric(
-                name="BST",
-                description="Beamlet statistics",
-            ),
-            grafana_apiv3_pb2.ListMetricsResponse.Metric(
-                name="SST",
-                description="Subband statistics",
-            ),
-            grafana_apiv3_pb2.ListMetricsResponse.Metric(
-                name="XST",
-                description="Crosslet statistics",
-            ),
-        ]
-
-        return grafana_apiv3_pb2.ListMetricsResponse(Metrics=metrics)
-
-    @call_exception_metrics("GrafanaAPIV3")
-    def GetMetricValue(self, request: grafana_apiv3_pb2.GetMetricValueRequest, context):
-        return grafana_apiv3_pb2.GetMetricValueResponse(frames=[])
-
-    @call_exception_metrics("GrafanaAPIV3")
-    def GetMetricAggregate(
-        self, request: grafana_apiv3_pb2.GetMetricAggregateRequest, context
-    ):
-        """Return the set of values for the request metrics and dimensions."""
-
-        frames = []
-
-        dimensions = {d.key: d.value for d in request.dimensions}
-        time_window = (
-            request.startDate.ToDatetime(tzinfo=timezone.utc),
-            request.endDate.ToDatetime(tzinfo=timezone.utc),
-        )
-
-        for metric in request.metrics:
-            if metric == "BST":
-                frames.extend(
-                    self.bst.all_frames(
-                        time_window,
-                        dimensions["antenna_field"].lower(),
-                        dimensions.get("pol"),
-                    )
-                )
-            elif metric == "SST":
-                frames.extend(
-                    self.sst.all_frames(
-                        time_window,
-                        dimensions["antenna_field"].lower(),
-                        dimensions.get("pol"),
-                    )
-                )
-            elif metric == "XST":
-                frames.extend(
-                    self.xst.all_frames(
-                        time_window,
-                        dimensions["antenna_field"].lower(),
-                        dimensions.get("pol"),
-                    )
-                )
-
-        return grafana_apiv3_pb2.GetMetricAggregateResponse(frames=frames)
diff --git a/tangostationcontrol/tangostationcontrol/rpc/observation.py b/tangostationcontrol/tangostationcontrol/rpc/observation.py
index 967a58d8b6b6484b7331ddc83f5eee2927e0f6fd..62d049574f1bd8985bb150b49d375c6625701dd1 100644
--- a/tangostationcontrol/tangostationcontrol/rpc/observation.py
+++ b/tangostationcontrol/tangostationcontrol/rpc/observation.py
@@ -2,8 +2,8 @@
 # SPDX-License-Identifier: Apache-2.0
 
 import logging
-from tangostationcontrol.rpc._proto import observation_pb2
-from tangostationcontrol.rpc._proto import observation_pb2_grpc
+from lofar_sid.interface.stationcontrol import observation_pb2
+from lofar_sid.interface.stationcontrol import observation_pb2_grpc
 from tangostationcontrol.rpc.common import (
     call_exception_metrics,
     reply_on_exception,
diff --git a/tangostationcontrol/tangostationcontrol/rpc/server.py b/tangostationcontrol/tangostationcontrol/rpc/server.py
index 4130b21e55f365b0369458c12c78dc3b7fb19d1b..e4dac367a7a8a24904ccc4e387b192c95cad04ef 100644
--- a/tangostationcontrol/tangostationcontrol/rpc/server.py
+++ b/tangostationcontrol/tangostationcontrol/rpc/server.py
@@ -8,19 +8,17 @@ import sys
 
 import grpc
 from grpc_reflection.v1alpha import reflection
-from tangostationcontrol.rpc._proto import observation_pb2
-from tangostationcontrol.rpc._proto import observation_pb2_grpc
-from tangostationcontrol.rpc._proto import statistics_pb2
-from tangostationcontrol.rpc._proto import statistics_pb2_grpc
-from tangostationcontrol.rpc._proto import grafana_apiv3_pb2
-from tangostationcontrol.rpc._proto import grafana_apiv3_pb2_grpc
+from lofar_sid.interface.stationcontrol import observation_pb2
+from lofar_sid.interface.stationcontrol import observation_pb2_grpc
+from lofar_sid.interface.stationcontrol import statistics_pb2
+from lofar_sid.interface.stationcontrol import statistics_pb2_grpc
+from lofar_sid.interface.stationcontrol import antennafield_pb2
+from lofar_sid.interface.stationcontrol import antennafield_pb2_grpc
 from tangostationcontrol.rpc.observation import Observation
 from tangostationcontrol.rpc.statistics import Statistics
-from tangostationcontrol.rpc.grafana_api import GrafanaAPIV3
 from tangostationcontrol.rpc.messagehandler import MultiEndpointZMQMessageHandler
 from tangostationcontrol.common.lofar_logging import configure_logger
 from tangostationcontrol.metrics import start_metrics_server
-from tangostationcontrol.rpc._proto import antennafield_pb2, antennafield_pb2_grpc
 from tangostationcontrol.rpc.antennafield import AntennaField
 
 logger = logging.getLogger()
@@ -43,14 +41,10 @@ class Server:
         statistics_pb2_grpc.add_StatisticsServicer_to_server(
             self.statistics_servicer, self.server
         )
-        grafana_apiv3_pb2_grpc.add_GrafanaQueryAPIServicer_to_server(
-            GrafanaAPIV3(self.statistics_servicer, antenna_fields), self.server
-        )
         SERVICE_NAMES = (
             observation_pb2.DESCRIPTOR.services_by_name["Observation"].full_name,
             antennafield_pb2.DESCRIPTOR.services_by_name["Antennafield"].full_name,
             statistics_pb2.DESCRIPTOR.services_by_name["Statistics"].full_name,
-            grafana_apiv3_pb2.DESCRIPTOR.services_by_name["GrafanaQueryAPI"].full_name,
             reflection.SERVICE_NAME,  # reflection is required by innius-gpc-datasource
         )
         reflection.enable_server_reflection(SERVICE_NAMES, self.server)
diff --git a/tangostationcontrol/tangostationcontrol/rpc/statistics.py b/tangostationcontrol/tangostationcontrol/rpc/statistics.py
index a5e51652a74776d99efeecadd6d1eed6ca7b5bad..5d46c69a489dd8f0fc832bb2f237916e1914007a 100644
--- a/tangostationcontrol/tangostationcontrol/rpc/statistics.py
+++ b/tangostationcontrol/tangostationcontrol/rpc/statistics.py
@@ -3,14 +3,15 @@
 
 from datetime import datetime, timedelta, timezone
 from math import log
-from typing import Dict, Tuple
+from typing import Dict, Tuple, Callable
 import logging
 
 import numpy
 
 from tangostationcontrol.common.constants import N_pol, N_subbands
-from tangostationcontrol.rpc._proto import statistics_pb2
-from tangostationcontrol.rpc._proto import statistics_pb2_grpc
+from tangostationcontrol.common.frequency_bands import Band, bands
+from lofar_sid.interface.stationcontrol import statistics_pb2
+from lofar_sid.interface.stationcontrol import statistics_pb2_grpc
 from tangostationcontrol.rpc.common import (
     call_exception_metrics,
 )
@@ -88,6 +89,23 @@ class Statistics(statistics_pb2_grpc.StatisticsServicer, LastStatisticsMessagesM
             nyquist_zone=message["source_info"]["nyquist_zone_index"],
         )
 
+    @staticmethod
+    def _subband_frequency_func(
+        frequency_band: statistics_pb2.FrequencyBand,
+    ) -> Callable[[int], float]:
+        """Return a function that converts a subband number into its central frequency (in Hz).
+
+        NB: Spectral inversion is assumed to have been configured correctly at time of recording.
+        """
+
+        band_name = Band.lookup_nyquist_zone(
+            frequency_band.antenna_type,
+            frequency_band.clock,
+            frequency_band.nyquist_zone,
+        )
+
+        return bands[band_name].subband_frequency
+
     @call_exception_metrics("Statistics")
     def Bst(self, request: statistics_pb2.BstRequest, context):
         bst_message = self.get_last_message("bst", request.antenna_field)
@@ -119,9 +137,13 @@ class Statistics(statistics_pb2_grpc.StatisticsServicer, LastStatisticsMessagesM
         sst_data = numpy.array(sst_message["sst_data"], dtype=numpy.float32)
         sst_data = sst_data.reshape((-1, N_pol, N_subbands))
 
+        frequency_band = self._message_to_frequency_band(sst_message)
+        subband_frequency = self._subband_frequency_func(frequency_band)
+
         subbands = [
             statistics_pb2.SstResult.SstSubband(
                 subband=subband_nr,
+                frequency=subband_frequency(subband_nr),
                 antennas=[
                     statistics_pb2.SstResult.SstSubband.SstAntenna(
                         antenna=antenna_nr,
@@ -137,7 +159,7 @@ class Statistics(statistics_pb2_grpc.StatisticsServicer, LastStatisticsMessagesM
         return statistics_pb2.SstReply(
             result=statistics_pb2.SstResult(
                 timestamp=datetime.fromisoformat(sst_message["timestamp"]),
-                frequency_band=self._message_to_frequency_band(sst_message),
+                frequency_band=frequency_band,
                 integration_interval=sst_message["integration_interval"],
                 subbands=subbands,
             ),
@@ -187,12 +209,16 @@ class Statistics(statistics_pb2_grpc.StatisticsServicer, LastStatisticsMessagesM
             if antenna1 <= antenna2
         ]
 
+        frequency_band = self._message_to_frequency_band(xst_message)
+        subband_frequency = self._subband_frequency_func(frequency_band)
+
         return statistics_pb2.XstReply(
             result=statistics_pb2.XstResult(
                 timestamp=datetime.fromisoformat(xst_message["timestamp"]),
-                frequency_band=self._message_to_frequency_band(xst_message),
+                frequency_band=frequency_band,
                 integration_interval=xst_message["integration_interval"],
                 subband=xst_message["subband"],
+                frequency=subband_frequency(xst_message["subband"]),
                 baselines=baselines,
             ),
         )
diff --git a/tangostationcontrol/test/rpc/test_antennafield.py b/tangostationcontrol/test/rpc/test_antennafield.py
index 8bfb89204d1a1bebf7f0b44a63dd60fe65c684ef..90089d928695f022904cc6bcc2b3f0dfb36e92d1 100644
--- a/tangostationcontrol/test/rpc/test_antennafield.py
+++ b/tangostationcontrol/test/rpc/test_antennafield.py
@@ -2,7 +2,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 from unittest.mock import MagicMock, patch
-from tangostationcontrol.rpc._proto.antennafield_pb2 import (
+from lofar_sid.interface.stationcontrol.antennafield_pb2 import (
     Identifier,
     SetAntennaStatusRequest,
     SetAntennaUseRequest,
diff --git a/tangostationcontrol/test/rpc/test_grafana_apiv3.py b/tangostationcontrol/test/rpc/test_grafana_apiv3.py
deleted file mode 100644
index b57f11ac6b2f99dc3cd68a90a70895b2b59b3409..0000000000000000000000000000000000000000
--- a/tangostationcontrol/test/rpc/test_grafana_apiv3.py
+++ /dev/null
@@ -1,369 +0,0 @@
-# Copyright (C) 2025 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
-
-from datetime import datetime, timezone, timedelta
-import json
-from os import path
-
-from tangostationcontrol.common.constants import (
-    N_subbands,
-    N_pol,
-    N_beamlets_ctrl,
-    N_pn,
-    S_pn,
-    A_pn,
-)
-from tangostationcontrol.rpc.grafana_api import (
-    GrafanaAPIV3,
-    StatisticsToGrafana,
-    BstToGrafana,
-    SstToGrafana,
-    XstToGrafana,
-)
-from tangostationcontrol.rpc._proto import grafana_apiv3_pb2
-from tangostationcontrol.rpc.statistics import (
-    Statistics,
-    dB,
-    NotAvailableError,
-    TooOldError,
-)
-from tangostationcontrol.rpc._proto import statistics_pb2
-
-from google.protobuf.timestamp_pb2 import Timestamp
-
-from test import base
-
-
-def field_labels(label_list: list[grafana_apiv3_pb2.Label]) -> dict[str, str]:
-    """Turn a list of objects with key and value fields into a dict."""
-    return {x.key: x.value for x in label_list}
-
-
-class TestStatisticsToGrafana(base.TestCase):
-    def test_verify_call_result(self):
-        statistics = Statistics()
-        sut = StatisticsToGrafana(statistics)
-
-        now = datetime.now(tz=timezone.utc)
-
-        reply = statistics_pb2.SstReply(result=statistics_pb2.SstResult(timestamp=now))
-
-        # should not raise
-        _ = sut._verify_call_result(
-            reply, (now - timedelta(seconds=1), now + timedelta(seconds=1))
-        )
-
-        # too old raises
-        with self.assertRaises(TooOldError):
-            _ = sut._verify_call_result(
-                reply, (now + timedelta(seconds=1), now + timedelta(seconds=2))
-            )
-
-
-class TestBstToGrafana(base.TestCase):
-    def setUp(self):
-        statistics = Statistics()
-        self.sut = BstToGrafana(statistics)
-
-        # provide with sample statistic
-        with open(f"{path.dirname(__file__)}/bst-message.json") as f:
-            self.bst_message = json.load(f)
-            statistics.handle_statistics_message(
-                "bst/lba/cs032", datetime.now(), self.bst_message
-            )
-
-        self.valid_time_window = (
-            datetime.fromisoformat("2025-01-01T00:00+00:00"),
-            datetime.fromisoformat("2026-01-01T00:00+00:00"),
-        )
-
-    def test_all_frames(self):
-        frames = self.sut.all_frames(self.valid_time_window, "lba", None)
-
-        self.assertEqual(1, len(frames))
-        self.assertEqual("BST", frames[0].metric)
-        self.assertEqual(3, len(frames[0].fields))
-        self.assertEqual("beamlet", frames[0].fields[0].name)
-        self.assertEqual("xx", frames[0].fields[1].name)
-        self.assertEqual("yy", frames[0].fields[2].name)
-
-        # all fields must be of the right size
-        self.assertEqual(N_beamlets_ctrl, len(frames[0].timestamps))
-        self.assertEqual(N_beamlets_ctrl, len(frames[0].fields[0].values))
-        self.assertEqual(N_beamlets_ctrl, len(frames[0].fields[1].values))
-        self.assertEqual(N_beamlets_ctrl, len(frames[0].fields[2].values))
-
-        # all timestamps must be equal
-        self.assertEqual(1, len(set([ts.ToDatetime() for ts in frames[0].timestamps])))
-
-        # all beamlets must be there
-        self.assertEqual(set(range(N_beamlets_ctrl)), set(frames[0].fields[0].values))
-
-        # test values
-        for beamlet_idx, value in enumerate(frames[0].fields[1].values):
-            self.assertAlmostEqual(
-                dB(self.bst_message["bst_data"][beamlet_idx][0]),
-                value,
-                5,
-                msg=f"{beamlet_idx=}",
-            )
-        for beamlet_idx, value in enumerate(frames[0].fields[2].values):
-            self.assertAlmostEqual(
-                dB(self.bst_message["bst_data"][beamlet_idx][1]),
-                value,
-                5,
-                msg=f"{beamlet_idx=}",
-            )
-
-
-class TestSstToGrafana(base.TestCase):
-    def setUp(self):
-        statistics = Statistics()
-        self.sut = SstToGrafana(statistics)
-
-        # provide with sample statistic
-        with open(f"{path.dirname(__file__)}/sst-message.json") as f:
-            self.sst_message = json.load(f)
-            statistics.handle_statistics_message(
-                "sst/lba/cs032", datetime.now(), self.sst_message
-            )
-
-        self.valid_time_window = (
-            datetime.fromisoformat("2025-01-01T00:00+00:00"),
-            datetime.fromisoformat("2026-01-01T00:00+00:00"),
-        )
-
-    def test_all_frames(self):
-        frames = self.sut.all_frames(self.valid_time_window, "lba", None)
-
-        self.assertEqual(1, len(frames))
-        self.assertEqual("SST", frames[0].metric)
-        self.assertEqual(1 + N_pn * S_pn, len(frames[0].fields))
-        self.assertEqual("frequency", frames[0].fields[0].name)
-
-        for idx, field in enumerate(frames[0].fields[1:], 1):
-            self.assertEqual("power", field.name, msg=f"{idx=}")
-
-        # all fields must be of the right size
-        self.assertEqual(N_subbands - 1, len(frames[0].timestamps))
-        self.assertEqual(N_subbands - 1, len(frames[0].fields[0].values))
-
-        for idx, field in enumerate(frames[0].fields[1:], 1):
-            self.assertEqual(N_subbands - 1, len(field.values), msg=f"{idx=}")
-
-        # all timestamps must be equal
-        self.assertEqual(1, len(set([ts.ToDatetime() for ts in frames[0].timestamps])))
-
-        # all inputs must be there
-        input_names = {
-            field_labels(field.labels)["antenna"] for field in frames[0].fields[1:]
-        }
-        self.assertEqual(N_pn * A_pn, len(input_names), msg=f"{input_names=}")
-        # both polarisarions must be there
-        input_pols = [
-            field_labels(field.labels)["pol"] for field in frames[0].fields[1:]
-        ]
-        self.assertEqual(N_pn * A_pn, input_pols.count("xx"), msg=f"{input_pols=}")
-        self.assertEqual(N_pn * A_pn, input_pols.count("yy"), msg=f"{input_pols=}")
-
-        # test values
-        for field in frames[0].fields[1:]:
-            labels = field_labels(field.labels)
-
-            antenna_idx = int(labels["antenna"])
-            pol_indices = {"xx": 0, "yy": 1}
-            pol_idx = pol_indices[labels["pol"]]
-            input_idx = antenna_idx * N_pol + pol_idx
-
-            for idx, value in enumerate(field.values):
-                self.assertAlmostEqual(
-                    dB(self.sst_message["sst_data"][input_idx][idx + 1]),
-                    value,
-                    5,
-                    msg=f"{labels=}, {idx=}",
-                )
-
-
-class TestXstToGrafana(base.TestCase):
-    def setUp(self):
-        statistics = Statistics()
-        self.sut = XstToGrafana(statistics)
-
-        # provide with sample statistic
-        with open(f"{path.dirname(__file__)}/xst-message.json") as f:
-            self.xst_message = json.load(f)
-            statistics.handle_statistics_message(
-                "xst/lba/cs032", datetime.now(), self.xst_message
-            )
-
-        self.valid_time_window = (
-            datetime.fromisoformat("2025-01-01T00:00+00:00"),
-            datetime.fromisoformat("2026-01-01T00:00+00:00"),
-        )
-
-    def test_all_frames(self):
-        frames = self.sut.all_frames(self.valid_time_window, "lba", None)
-
-        self.assertEqual(1, len(frames))
-        self.assertEqual("XST", frames[0].metric)
-        self.assertEqual(3 + 2 * N_pol**2, len(frames[0].fields))
-        self.assertEqual("antenna1", frames[0].fields[0].name)
-        self.assertEqual("antenna2", frames[0].fields[1].name)
-        self.assertEqual("frequency", frames[0].fields[2].name)
-        self.assertEqual(
-            {"power", "phase"}, {field.name for field in frames[0].fields[3:]}
-        )
-
-        N_antennas = N_pn * A_pn
-        N_baselines = N_antennas * (N_antennas + 1) // 2
-
-        # all fields must be of the right size
-        self.assertEqual(N_baselines, len(frames[0].timestamps))
-
-        for idx, field in enumerate(frames[0].fields):
-            self.assertEqual(N_baselines, len(field.values), msg=f"{idx=}")
-
-        # all timestamps must be equal
-        self.assertEqual(1, len(set([ts.ToDatetime() for ts in frames[0].timestamps])))
-
-        # all polarisarions must be there (for power and phase)
-        input_pols = [
-            field_labels(field.labels)["pol"] for field in frames[0].fields[3:]
-        ]
-        self.assertEqual(2, input_pols.count("xx"))
-        self.assertEqual(2, input_pols.count("xy"))
-        self.assertEqual(2, input_pols.count("yx"))
-        self.assertEqual(2, input_pols.count("yy"))
-
-        # all antennas must be there
-        self.assertEqual(set(range(N_antennas)), set(frames[0].fields[0].values))
-        self.assertEqual(set(range(N_antennas)), set(frames[0].fields[1].values))
-
-        # test specific value for regression
-        self.assertAlmostEqual(30.7213135, frames[0].fields[3].values[99], 6)
-        self.assertAlmostEqual(3.88297279, frames[0].fields[4].values[99], 6)
-        self.assertAlmostEqual(27.2744141, frames[0].fields[5].values[99], 6)
-        self.assertAlmostEqual(297.7411965, frames[0].fields[6].values[99], 6)
-
-
-class TestGrafanaAPIV3(base.TestCase):
-    def test_getqueryoptions(self):
-        """Test GetQueryOptions."""
-
-        statistics = Statistics()
-        sut = GrafanaAPIV3(statistics, [])
-
-        request = grafana_apiv3_pb2.GetOptionsRequest()
-        reply = sut.GetQueryOptions(request, None)
-
-        self.assertEqual(0, len(reply.options))
-
-    def test_listdimensionkeys(self):
-        """Test ListDimensionKeys."""
-
-        statistics = Statistics()
-        sut = GrafanaAPIV3(statistics, [])
-
-        request = grafana_apiv3_pb2.ListDimensionKeysRequest()
-        reply = sut.ListDimensionKeys(request, None)
-
-        dimensions = {d.key for d in reply.results}
-
-        self.assertIn("antenna_field", dimensions)
-        self.assertIn("pol", dimensions)
-
-    def test_listdimensionvalues(self):
-        """Test ListDimensionValues."""
-
-        statistics = Statistics()
-        sut = GrafanaAPIV3(statistics, ["LBA", "HBA"])
-
-        expected_values = {
-            "antenna_field": {"LBA", "HBA"},
-            "pol": {"xx", "xy", "yx", "yy"},
-        }
-
-        for dim, expected_value in expected_values.items():
-            request = grafana_apiv3_pb2.ListDimensionValuesRequest(dimension_key=dim)
-            reply = sut.ListDimensionValues(request, None)
-
-            value = set([r.value for r in reply.results])
-            self.assertEqual(expected_value, value, msg=f"{dim=}")
-
-    def test_listmetrics(self):
-        """Test ListMetrics."""
-
-        statistics = Statistics()
-        sut = GrafanaAPIV3(statistics, [])
-
-        request = grafana_apiv3_pb2.ListMetricsRequest()
-        reply = sut.ListMetrics(request, None)
-
-        metrics = [m.name for m in reply.Metrics]
-
-        self.assertIn("BST", metrics)
-        self.assertIn("SST", metrics)
-        self.assertIn("XST", metrics)
-
-
-class TestGetMetricAggregate(base.TestCase):
-    def setUp(self):
-        statistics = Statistics()
-        self.sut = GrafanaAPIV3(statistics, ["LBA"])
-
-        # provide with sample statistic
-        with open(f"{path.dirname(__file__)}/sst-message.json") as f:
-            sst_message = json.load(f)
-            statistics.handle_statistics_message(
-                "sst/lba/cs032", datetime.now(), sst_message
-            )
-
-    def test_nometrics(self):
-        """Test if there are no metrics available."""
-
-        statistics = Statistics()
-        sut = GrafanaAPIV3(statistics, ["LBA"])
-
-        # request/response
-        request = grafana_apiv3_pb2.GetMetricAggregateRequest(
-            metrics=["SST"],
-            dimensions=[grafana_apiv3_pb2.Dimension(key="antenna_field", value="LBA")],
-            startDate=Timestamp(seconds=0),
-            endDate=Timestamp(seconds=int(datetime.now().timestamp())),
-        )
-
-        with self.assertRaises(NotAvailableError):
-            _ = sut.GetMetricAggregate(request, None)
-
-    def test_validdaterange(self):
-        """Test obtaining SSTs using a date range for which they are available."""
-
-        # request/response
-        request = grafana_apiv3_pb2.GetMetricAggregateRequest(
-            metrics=["SST"],
-            dimensions=[grafana_apiv3_pb2.Dimension(key="antenna_field", value="LBA")],
-            startDate=Timestamp(seconds=0),
-            endDate=Timestamp(seconds=int(datetime.now().timestamp())),
-        )
-
-        reply = self.sut.GetMetricAggregate(request, None)
-
-        # validate output
-        self.assertEqual(1, len(reply.frames))
-        self.assertEqual("SST", reply.frames[0].metric)
-
-    def test_tooold(self):
-        """Test obtaining SSTs using a date range for which they are too old."""
-
-        # request/response
-        request = grafana_apiv3_pb2.GetMetricAggregateRequest(
-            metrics=["SST"],
-            dimensions=[grafana_apiv3_pb2.Dimension(key="antenna_field", value="LBA")],
-            startDate=Timestamp(seconds=int(datetime.now().timestamp()) - 1),
-            endDate=Timestamp(seconds=int(datetime.now().timestamp())),
-        )
-
-        reply = self.sut.GetMetricAggregate(request, None)
-
-        # validate output
-        self.assertEqual(0, len(reply.frames))
diff --git a/tangostationcontrol/test/rpc/test_observation.py b/tangostationcontrol/test/rpc/test_observation.py
index 2da9fba52c5818c5f7731cb58a2d0b31f0c3d665..9d6a3915c7ec3173bdb15d8b1447176d194e4411 100644
--- a/tangostationcontrol/test/rpc/test_observation.py
+++ b/tangostationcontrol/test/rpc/test_observation.py
@@ -4,7 +4,7 @@
 from unittest import mock
 
 from tangostationcontrol.rpc.observation import Observation
-from tangostationcontrol.rpc._proto import observation_pb2
+from lofar_sid.interface.stationcontrol import observation_pb2
 
 from test import base
 
diff --git a/tangostationcontrol/test/rpc/test_server.py b/tangostationcontrol/test/rpc/test_server.py
index 73bda154167c53fb83bdbed76a7ee7de49ddb26f..d7bbc4c88467e42e7c2c531557dddd8d385d38cf 100644
--- a/tangostationcontrol/test/rpc/test_server.py
+++ b/tangostationcontrol/test/rpc/test_server.py
@@ -8,8 +8,8 @@ from grpc_reflection.v1alpha.proto_reflection_descriptor_database import (
     ProtoReflectionDescriptorDatabase,
 )
 
-from tangostationcontrol.rpc._proto import grafana_apiv3_pb2
-from tangostationcontrol.rpc._proto import grafana_apiv3_pb2_grpc
+from lofar_sid.interface.stationcontrol import antennafield_pb2
+from lofar_sid.interface.stationcontrol import antennafield_pb2_grpc
 from tangostationcontrol.rpc.server import Server
 
 from test import base
@@ -36,11 +36,17 @@ class TestServer(base.TestCase):
             self.assertIn("Observation", services)
             self.assertIn("Antennafield", services)
             self.assertIn("Statistics", services)
-            self.assertIn("grafanav3.GrafanaQueryAPI", services)
 
     def test_call(self):
         """Test a basic gRPC call to the server."""
 
         with grpc.insecure_channel(f"localhost:{self.server.port}") as channel:
-            stub = grafana_apiv3_pb2_grpc.GrafanaQueryAPIStub(channel)
-            _ = stub.GetQueryOptions(grafana_apiv3_pb2.GetOptionsRequest())
+            stub = antennafield_pb2_grpc.AntennafieldStub(channel)
+
+            identifier = antennafield_pb2.Identifier(
+                antennafield_name="lba",
+                antenna_name="LBA00",
+            )
+            _ = stub.GetAntenna(
+                antennafield_pb2.GetAntennaRequest(identifier=identifier)
+            )
diff --git a/tangostationcontrol/test/rpc/test_statistics.py b/tangostationcontrol/test/rpc/test_statistics.py
index 141aebb52f1e454f59bf91f94640059c74dc9457..3b9f90373b93cb89d604e124d99ab8c6d88d1b1e 100644
--- a/tangostationcontrol/test/rpc/test_statistics.py
+++ b/tangostationcontrol/test/rpc/test_statistics.py
@@ -20,7 +20,7 @@ from tangostationcontrol.rpc.statistics import (
     TooOldError,
     NotAvailableError,
 )
-from tangostationcontrol.rpc._proto import statistics_pb2
+from lofar_sid.interface.stationcontrol import statistics_pb2
 
 from test import base
 
diff --git a/tangostationcontrol/tox.ini b/tangostationcontrol/tox.ini
index 090286a2a5a32c2a804d3a619cd93ff2ee52274e..4e414a875eddc076380c180b5b364e7554ffd785 100644
--- a/tangostationcontrol/tox.ini
+++ b/tangostationcontrol/tox.ini
@@ -29,10 +29,6 @@ allowlist_externals =
 commands_pre =
     {envpython} --version
     {work_dir}/.tox/bin/python -m tox --version
-    {envpython} -m grpc_tools.protoc -Itangostationcontrol/rpc/_proto=proto --python_out=. --pyi_out=. --grpc_python_out=. proto/observation.proto
-    {envpython} -m grpc_tools.protoc -Itangostationcontrol/rpc/_proto=proto --python_out=. --pyi_out=. --grpc_python_out=. proto/antennafield.proto
-    {envpython} -m grpc_tools.protoc -Itangostationcontrol/rpc/_proto=proto --python_out=. --pyi_out=. --grpc_python_out=. proto/statistics.proto
-    {envpython} -m grpc_tools.protoc -Itangostationcontrol/rpc/_proto=proto --python_out=. --pyi_out=. --grpc_python_out=. proto/grafana-apiv3.proto
 commands =
     {envpython} -m pytest --version
     {envpython} -m pytest -v --log-level=DEBUG --forked test/{posargs}
@@ -124,4 +120,4 @@ commands =
 [flake8]
 filename = *.py,.stestr.conf,.txt
 ignore = B014, B019, B028, W291, W293, W391, E111, E114, E121, E122, E123, E124, E126, E127, E128, E131, E201, E201, E202, E203, E221, E222, E225, E226, E231, E241, E251, E252, E261, E262, E265, E271, E301, E302, E303, E305, E306, E401, E402, E501, E502, E701, E712, E721, E731, F403, F523, F541, F841, H301, H306, H401, H403, H404, H405, W503
-exclude = SNMP_mib_loading,output_pymibs,_proto
+exclude = SNMP_mib_loading,output_pymibs