From acac49d4f8151f627f2ef4b9ad8b7105c34637fd Mon Sep 17 00:00:00 2001
From: Jan David Mol <mol@astron.nl>
Date: Tue, 2 Apr 2024 07:13:02 +0000
Subject: [PATCH] Change hardware_powered_R into a fraction to see partial
 powerups, and support it for more devices

---
 README.md                                     |  2 ++
 tangostationcontrol/VERSION                   |  2 +-
 .../tangostationcontrol/devices/apsct.py      |  4 +--
 .../base_device_classes/beam_device.py        | 10 +++++---
 .../base_device_classes/lofar_device.py       | 14 ++++++-----
 .../base_device_classes/recv_device.py        | 11 ++++++++
 .../tangostationcontrol/devices/ccd.py        |  4 +--
 .../tangostationcontrol/devices/ec.py         |  4 +--
 .../devices/sdp/beamlet.py                    |  4 +++
 .../tangostationcontrol/devices/sdp/bst.py    |  4 +++
 .../devices/sdp/firmware.py                   | 13 ++++++++++
 .../tangostationcontrol/devices/sdp/sdp.py    | 11 ++++++++
 .../tangostationcontrol/devices/sdp/sst.py    |  4 +++
 .../tangostationcontrol/devices/sdp/xst.py    | 25 +++++++++++++++++++
 .../tangostationcontrol/devices/unb2.py       | 16 +++++++++---
 15 files changed, 108 insertions(+), 20 deletions(-)

diff --git a/README.md b/README.md
index 01a1f0f3b..65d47484f 100644
--- a/README.md
+++ b/README.md
@@ -166,6 +166,8 @@ Next change the version in the following places:
 
 # Release Notes
 
+* 0.32.2 Change hardware_powered_R to hardware_powered_fraction_R to report partial power.
+         Implemented hardware_powered_fraction_R for more devices.
 * 0.32.1 Do not serve stale metrics
 * 0.32.0 Add available_in_power_state_R attribute to determine from which station state a device will be available
 * 0.31.4 Bugfixes for DTS configuration,
diff --git a/tangostationcontrol/VERSION b/tangostationcontrol/VERSION
index fd9620c08..989b29cc3 100644
--- a/tangostationcontrol/VERSION
+++ b/tangostationcontrol/VERSION
@@ -1 +1 @@
-0.32.1
+0.32.2
diff --git a/tangostationcontrol/tangostationcontrol/devices/apsct.py b/tangostationcontrol/tangostationcontrol/devices/apsct.py
index b17a82068..7c7c1890e 100644
--- a/tangostationcontrol/tangostationcontrol/devices/apsct.py
+++ b/tangostationcontrol/tangostationcontrol/devices/apsct.py
@@ -204,9 +204,9 @@ class APSCT(OPCUADevice):
     # overloaded functions
     # --------
 
-    def _read_hardware_powered_R(self):
+    def _read_hardware_powered_fraction_R(self):
         """Read attribute which monitors the power"""
-        return self.read_attribute("APSCT_PWR_on_R")
+        return 1.0 * self.read_attribute("APSCT_PWR_on_R")
 
     def _power_hardware_on(self):
         """Turns on the 200MHz clock."""
diff --git a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/beam_device.py b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/beam_device.py
index ddb2773d6..5122e67ec 100644
--- a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/beam_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/beam_device.py
@@ -156,14 +156,14 @@ class BeamDevice(AsyncDevice):
 
     Tracking_enabled_R = attribute(
         access=AttrWriteType.READ,
-        doc="Whether the tile beam is updated periodically",
+        doc="Whether the beam is updated periodically",
         dtype=bool,
         fget=lambda self: bool(self.Beam_tracker and self.Beam_tracker.is_alive()),
     )
 
     Tracking_enabled_RW = attribute(
         access=AttrWriteType.READ_WRITE,
-        doc="Whether the tile beam should be updated periodically",
+        doc="Whether the beam should be updated periodically",
         dtype=bool,
         fget=lambda self: self._tracking_enabled_rw,
     )
@@ -218,7 +218,7 @@ class BeamDevice(AsyncDevice):
         # store the new values
         self._beam_manager.new_pointing_direction = numpy.array(value, dtype="<U32")
 
-        # force update across tiles if pointing changes
+        # force update if pointing changes
         self.Beam_tracker.force_update()
         logger.info("Pointing direction update requested")
 
@@ -234,6 +234,10 @@ class BeamDevice(AsyncDevice):
     # overloaded functions
     # --------
 
+    async def _read_hardware_powered_fraction_R(self):
+        # We consider 'powered' if the beam tracker is running
+        return 1.0 * await self.async_read_attribute("Tracking_enabled_R")
+
     def _init_device(self, beam_manager: AbstractBeamManager):
         super()._init_device()
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/lofar_device.py b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/lofar_device.py
index 8a8ede8e7..406046c85 100644
--- a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/lofar_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/lofar_device.py
@@ -226,9 +226,11 @@ class LOFARDevice(Device):
         fget=lambda self: numpy.int64(self._access_count),
     )
 
-    hardware_powered_R = attribute(
-        dtype=bool,
-        fget=lambda self: self._read_hardware_powered_R(),
+    hardware_powered_fraction_R = attribute(
+        doc="Fraction of hardware that is powered on (0 = nothing, 1 = all).",
+        dtype=numpy.float64,
+        fget=lambda self: self._read_hardware_powered_fraction_R(),
+        fisallowed="is_attribute_access_allowed",
     )
 
     event_thread_running_R = attribute(
@@ -647,11 +649,11 @@ class LOFARDevice(Device):
         # increase the number of accesses
         self._access_count += 1
 
-    def _read_hardware_powered_R(self):
-        """Overloadable function called in read attribute hardware_powered_R"""
+    def _read_hardware_powered_fraction_R(self):
+        """Overloadable function called to ask whether all hardware is powered."""
 
         # If device backs no hardware, assume it's powered
-        return True
+        return 1.0
 
     def properties_changed(self):
         pass
diff --git a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/recv_device.py b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/recv_device.py
index 3551c33cd..fff0279eb 100644
--- a/tangostationcontrol/tangostationcontrol/devices/base_device_classes/recv_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/base_device_classes/recv_device.py
@@ -386,6 +386,17 @@ class RECVDevice(OPCUADevice):
     # overloaded functions
     # --------
 
+    def _read_hardware_powered_fraction_R(self):
+        """Read attribute which monitors the power"""
+
+        mask = self.read_attribute("RCU_mask_RW")
+        powered = self.read_attribute("RCU_PWR_good_R")
+
+        try:
+            return numpy.count_nonzero(powered & mask) / numpy.count_nonzero(mask)
+        except ZeroDivisionError:
+            return 1.0
+
     def _power_hardware_on(self):
         """Power the RCUs."""
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/ccd.py b/tangostationcontrol/tangostationcontrol/devices/ccd.py
index 6eb0d9d6e..9aecfc11f 100644
--- a/tangostationcontrol/tangostationcontrol/devices/ccd.py
+++ b/tangostationcontrol/tangostationcontrol/devices/ccd.py
@@ -217,9 +217,9 @@ class CCD(OPCUADevice):
         self.wait_attribute("CCDTR_translator_busy_R", False, self.CCD_On_Off_timeout)
         logger.debug("Put CCD in off state")
 
-    def _read_hardware_powered_R(self):
+    def _read_hardware_powered_fraction_R(self):
         """Read attribute which monitors the power"""
-        return self.read_attribute("CCD_PWR_on_R")
+        return 1.0 * self.read_attribute("CCD_PWR_on_R")
 
     def _power_hardware_on(self):
         """Forward the clock signal."""
diff --git a/tangostationcontrol/tangostationcontrol/devices/ec.py b/tangostationcontrol/tangostationcontrol/devices/ec.py
index 3bc7354e2..85893daf0 100644
--- a/tangostationcontrol/tangostationcontrol/devices/ec.py
+++ b/tangostationcontrol/tangostationcontrol/devices/ec.py
@@ -113,11 +113,11 @@ class EC(OPCUADevice):
     # overloaded functions
     # --------
 
-    def _read_hardware_powered_R(self):
+    def _read_hardware_powered_fraction_R(self):
         # the device and translator are one, so if
         # the translator is reachable, the hardware
         # is powered.
-        return self.read_attribute("connected_R")
+        return 1.0 * self.read_attribute("connected_R")
 
     # --------
     # Commands
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py b/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py
index 56a716b2d..77e54b87e 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py
@@ -669,6 +669,10 @@ class Beamlet(OPCUADevice):
     # Overloaded functions
     # --------
 
+    def _read_hardware_powered_fraction_R(self):
+        """Read attribute which monitors the power"""
+        return 1.0 * any(self.read_attribute("FPGA_beamlet_output_enable_R"))
+
     def configure_for_initialise(self):
         super().configure_for_initialise()
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/bst.py b/tangostationcontrol/tangostationcontrol/devices/sdp/bst.py
index 256b082b8..9a259373d 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/bst.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/bst.py
@@ -195,6 +195,10 @@ class BST(Statistics):
     # Overloaded functions
     # --------
 
+    def _read_hardware_powered_fraction_R(self):
+        """Read attribute which monitors the power"""
+        return 1.0 * any(self.read_attribute("FPGA_bst_offload_enable_R"))
+
     def _power_hardware_on(self):
         self.proxy.write_attribute(
             "FPGA_bst_offload_enable_RW", self.FPGA_bst_offload_enable_RW_default
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/firmware.py b/tangostationcontrol/tangostationcontrol/devices/sdp/firmware.py
index cdfc9f221..fb136f35d 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/firmware.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/firmware.py
@@ -366,6 +366,19 @@ class SDPFirmware(OPCUADevice):
             self.Firmware_Boot_timeout,
         )
 
+    def _read_hardware_powered_fraction_R(self):
+        """Read attribute which monitors the power"""
+
+        boot_image = self.read_attribute("FPGA_boot_image_R")
+        mask = self.read_attribute("TR_fpga_mask_R")
+
+        try:
+            return numpy.count_nonzero((boot_image == 1) & mask) / numpy.count_nonzero(
+                mask
+            )
+        except ZeroDivisionError:
+            return 1.0
+
     def _power_hardware_on(self):
         """Boot the SDP Firmware user image"""
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
index 5baa9b113..76f0c0df9 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
@@ -555,6 +555,17 @@ class SDP(OPCUADevice):
     # overloaded functions
     # --------
 
+    def _read_hardware_powered_fraction_R(self):
+        """Read attribute which monitors the power"""
+
+        mask = self.control.read_parent_attribute("TR_fpga_mask_R")
+        powered = self.read_attribute("FPGA_processing_enable_R")
+
+        try:
+            return numpy.count_nonzero(powered & mask) / numpy.count_nonzero(mask)
+        except ZeroDivisionError:
+            return 1.0
+
     def configure_for_initialise(self):
         super().configure_for_initialise()
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/sst.py b/tangostationcontrol/tangostationcontrol/devices/sdp/sst.py
index 63d05a5cb..a237a3ac2 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/sst.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/sst.py
@@ -222,6 +222,10 @@ class SST(Statistics):
     # Overloaded functions
     # --------
 
+    def _read_hardware_powered_fraction_R(self):
+        """Read attribute which monitors the power"""
+        return 1.0 * any(self.read_attribute("FPGA_sst_offload_enable_R"))
+
     def _power_hardware_on(self):
         self.proxy.write_attribute(
             "FPGA_sst_offload_enable_RW", self.FPGA_sst_offload_enable_RW_default
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/xst.py b/tangostationcontrol/tangostationcontrol/devices/sdp/xst.py
index 45d7cbe3c..49f6407fa 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/xst.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/xst.py
@@ -722,6 +722,31 @@ class XST(Statistics):
     # Overloaded functions
     # --------
 
+    def _read_hardware_powered_fraction_R(self):
+        """Read attribute which monitors the power"""
+
+        processing_enabled = self.read_attribute("FPGA_xst_processing_enable_R")
+        offload_enabled = self.read_attribute("FPGA_xst_offload_enable_R")
+
+        expected_processing_enabled = numpy.array(
+            self.FPGA_xst_processing_enable_RW_default, dtype=bool
+        )
+        expected_offload_enabled = numpy.array(
+            self.FPGA_xst_offload_enable_RW_default, dtype=bool
+        )
+
+        mask = expected_processing_enabled | expected_offload_enabled
+
+        try:
+            # "powered" means processing and offload is as expected for the FPGAs required
+            return numpy.count_nonzero(
+                (processing_enabled == expected_processing_enabled)
+                & (offload_enabled == expected_offload_enabled)
+                & mask
+            ) / numpy.count_nonzero(mask)
+        except ZeroDivisionError:
+            return 1.0
+
     def _power_hardware_on(self):
         self.proxy.write_attribute(
             "FPGA_xst_processing_enable_RW", self.FPGA_xst_processing_enable_RW_default
diff --git a/tangostationcontrol/tangostationcontrol/devices/unb2.py b/tangostationcontrol/tangostationcontrol/devices/unb2.py
index 55d930972..85d7e4de3 100644
--- a/tangostationcontrol/tangostationcontrol/devices/unb2.py
+++ b/tangostationcontrol/tangostationcontrol/devices/unb2.py
@@ -427,10 +427,16 @@ class UNB2(OPCUADevice):
     # overloaded functions
     # --------
 
-    def _read_hardware_powered_R(self):
+    def _read_hardware_powered_fraction_R(self):
         """Read attribute which monitors the power"""
-        # Return True if all uniboards are powered
-        return all(self.read_attribute("UNB2_PWR_on_R"))
+
+        mask = self.read_attribute("UNB2_mask_RW")
+        powered = self.read_attribute("UNB2_PWR_on_R")
+
+        try:
+            return numpy.count_nonzero(powered & mask) / numpy.count_nonzero(mask)
+        except ZeroDivisionError:
+            return 1.0
 
     def _power_hardware_on(self):
         """Power the Uniboards."""
@@ -438,7 +444,9 @@ class UNB2(OPCUADevice):
         self.UNB2_on()
         self.wait_attribute("UNB2TR_translator_busy_R", False, self.UNB2_On_Off_timeout)
 
-        self.wait_attribute("hardware_powered_R", True, self.UNB2_On_Off_timeout)
+        self.wait_attribute(
+            "hardware_powered_fraction_R", 1.0, self.UNB2_On_Off_timeout
+        )
 
     def _power_hardware_off(self):
         """Disable the Uniboards."""
-- 
GitLab