From 9c28a9b3910ac4ebfc98e5a7a522777d5bf3fabe Mon Sep 17 00:00:00 2001
From: Parker Fagrelius <parfa30@gmail.com>
Date: Wed, 12 Mar 2025 12:11:12 -0300
Subject: [PATCH 1/2] tests

---
 .../maintel/park_calibration_projector.py     | 101 ++++++++++++++
 .../test_park_calibration_projector.py        | 123 ++++++++++++++++++
 2 files changed, 224 insertions(+)
 create mode 100644 python/lsst/ts/externalscripts/maintel/park_calibration_projector.py
 create mode 100644 tests/maintel/test_park_calibration_projector.py

diff --git a/python/lsst/ts/externalscripts/maintel/park_calibration_projector.py b/python/lsst/ts/externalscripts/maintel/park_calibration_projector.py
new file mode 100644
index 00000000..d5992915
--- /dev/null
+++ b/python/lsst/ts/externalscripts/maintel/park_calibration_projector.py
@@ -0,0 +1,101 @@
+# This file is part of ts_externalscripts
+#
+# Developed for the LSST Telescope and Site Systems.
+# This product includes software developed by the LSST Project
+# (https://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+__all__ = ["ParkCalibrationProjector"]
+
+
+from lsst.ts import salobj
+from lsst.ts.observatory.control.maintel.mtcalsys import MTCalsys
+
+
+class ParkCalibrationProjector(salobj.BaseScript):
+    """Move the calibration projector into a safe position
+
+    Parameters
+    ----------
+    index : int
+        Index of Script SAL component.
+    """
+
+    def __init__(self, index):
+        super().__init__(
+            index=index,
+            descr="Park Calibration Projector",
+        )
+
+        self.mtcalsys = None
+
+    def set_metadata(self, metadata):
+        metadata.duration = 30
+
+    async def configure(self, config):
+        """Configure the script.
+
+        Parameters
+        ----------
+        config : ``self.cmd_configure.DataType``
+
+        """
+        self.log.info("Configure started")
+        if self.mtcalsys is None:
+            self.log.debug("Creating MTCalSys.")
+            self.mtcalsys = MTCalsys(domain=self.domain, log=self.log)
+            await self.mtcalsys.start_task
+
+        self.linearstage_led_focus = self.mtcalsys.linearstage_led_focus
+        self.linearstage_led_select = self.mtcalsys.linearstage_led_select
+        self.linearstage_projector_select = self.mtcalsys.linearstage_projector_select
+        self.led_projector = self.mtcalsys.rem.ledprojector
+
+        self.log.info("Configure completed")
+
+    async def run(self):
+        """Run script."""
+        await self.assert_components_enabled()
+
+        self.log.info("Parking Calibration Projector")
+        await self.mtcalsys.park_projector()
+
+        # # TO-DO: DM-49065 for mtcalsys.py
+        # params = await self.mtcalsys.get_projector_setup()
+
+    async def assert_components_enabled(self):
+        """Checks if LEDProjector and all LinearStages are ENABLED
+        Raises
+        ------
+        RunTimeError:
+            If either component is not ENABLED"""
+
+        comps = [
+            self.linearstage_led_focus,
+            self.linearstage_led_select,
+            self.linearstage_projector_select,
+            self.led_projector,
+        ]
+
+        for comp in comps:
+            summary_state = await comp.evt_summaryState.aget()
+            try:
+                summaryState = summary_state.summaryState
+            except NameError:
+                summaryState = summary_state
+            if salobj.State(summaryState) != salobj.State(salobj.State.ENABLED):
+                raise RuntimeError(f"{comp} is not ENABLED")
diff --git a/tests/maintel/test_park_calibration_projector.py b/tests/maintel/test_park_calibration_projector.py
new file mode 100644
index 00000000..63c9bb9a
--- /dev/null
+++ b/tests/maintel/test_park_calibration_projector.py
@@ -0,0 +1,123 @@
+# This file is part of ts_externalscripts
+#
+# Developed for the LSST Telescope and Site Systems.
+# This product includes software developed by the LSST Project
+# (https://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+import logging
+import os
+import unittest
+
+from lsst.ts import externalscripts, salobj, standardscripts, utils
+from lsst.ts.externalscripts.maintel.park_calibration_projector import (
+    ParkCalibrationProjector,
+)
+from lsst.ts.observatory.control.maintel.mtcalsys import MTCalsys
+from lsst.ts.xml.enums import Script
+
+index_gen = utils.index_generator()
+
+
+class TestSetupWhiteFlats(
+    standardscripts.BaseScriptTestCase, unittest.IsolatedAsyncioTestCase
+):
+    def setUp(self):
+        self.log = logging.getLogger(__name__)
+        self.log.propagate = True
+
+    @property
+    def remote_group(self) -> MTCalsys:
+        """The remote_group property."""
+        return self.mtcalsys
+
+    async def basic_make_script(self, index):
+        self.log.debug("Starting basic_make script")
+        self.script = ParkCalibrationProjector(index=index)
+
+        self.log.debug("Finished initializing from basic_make_script")
+        return (self.script,)
+
+    async def mock_calsys(self):
+        """Mock Calsys CSCs"""
+
+        self.script.linearstage_led_focus = unittest.mock.AsyncMock()
+        self.script.linearstage_led_focus.evt_summaryState.aget = (
+            unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
+        )
+        self.script.linearstage_led_focus.evt_summaryState.summaryState = (
+            unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
+        )
+        self.script.linearstage_led_select = unittest.mock.AsyncMock()
+        self.script.linearstage_led_select.evt_summaryState.aget = (
+            unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
+        )
+        self.script.linearstage_led_select.evt_summaryState.summaryState = (
+            unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
+        )
+        self.script.linearstage_projector_select = unittest.mock.AsyncMock()
+        self.script.linearstage_projector_select.evt_summaryState.aget = (
+            unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
+        )
+        self.script.linearstage_projector_select.evt_summaryState.summaryState = (
+            unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
+        )
+        self.script.led_projector = unittest.mock.AsyncMock()
+        self.script.led_projector.evt_summaryState.aget = unittest.mock.AsyncMock(
+            return_value=salobj.State.ENABLED
+        )
+        self.script.led_projector.evt_summaryState.summaryState = (
+            unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
+        )
+        self.script.mtcalsys.tunablelaser = unittest.mock.AsyncMock()
+        self.script.mtcalsys.tunablelaser.evt_summaryState.aget = (
+            unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
+        )
+        self.script.mtcalsys.tunablelaser.evt_summaryState.summaryState = (
+            unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
+        )
+
+    async def test_configure(self):
+        async with self.make_script():
+            await self.configure_script()
+            assert self.script.state.state == Script.ScriptState.CONFIGURED
+
+    async def test_run_without_failures(self):
+        async with self.make_script():
+            await self.configure_script()
+            assert self.script.state.state == Script.ScriptState.CONFIGURED
+
+            self.log.debug("Starting Mtcalsys mocks")
+            await self.mock_mtcalsys()
+            await self.mock_calsys()
+
+            self.log.debug("Enable all CSCs")
+
+            # Run the script
+            self.log.debug("Running the script")
+            await self.run_script()
+            assert self.script.state.state == Script.ScriptState.DONE
+
+    async def test_executable(self):
+        scripts_dir = externalscripts.get_scripts_dir()
+        script_path = os.path.join(
+            scripts_dir, "maintel", "park_calibration_projector.py"
+        )
+        await self.check_executable(script_path)
+
+    if __name__ == "__main__":
+        unittest.main()

From aeab49249de2735011dbdaef232b7b25b8e5d834 Mon Sep 17 00:00:00 2001
From: Parker Fagrelius <parfa30@gmail.com>
Date: Mon, 17 Mar 2025 12:34:25 -0300
Subject: [PATCH 2/2] adding test and other files

---
 doc/news/DM-49346.feature.rst                 |  1 +
 .../maintel/park_calibration_projector.py     | 26 +++++++++++++++++++
 .../ts/externalscripts/maintel/__init__.py    |  1 +
 .../maintel/park_calibration_projector.py     | 20 ++++++++++++--
 .../test_park_calibration_projector.py        | 17 ++++++------
 5 files changed, 55 insertions(+), 10 deletions(-)
 create mode 100644 doc/news/DM-49346.feature.rst
 create mode 100755 python/lsst/ts/externalscripts/data/scripts/maintel/park_calibration_projector.py

diff --git a/doc/news/DM-49346.feature.rst b/doc/news/DM-49346.feature.rst
new file mode 100644
index 00000000..181b35a3
--- /dev/null
+++ b/doc/news/DM-49346.feature.rst
@@ -0,0 +1 @@
+Included a script that parks the calibration projector in a safe place and turns off the LEDs
diff --git a/python/lsst/ts/externalscripts/data/scripts/maintel/park_calibration_projector.py b/python/lsst/ts/externalscripts/data/scripts/maintel/park_calibration_projector.py
new file mode 100755
index 00000000..c75d04e7
--- /dev/null
+++ b/python/lsst/ts/externalscripts/data/scripts/maintel/park_calibration_projector.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+# This file is part of ts_externalscripts
+#
+# Developed for the LSST Telescope and Site Systems.
+# This product includes software developed by the LSST Project
+# (https://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+
+import asyncio
+
+from lsst.ts.externalscripts.maintel import ParkCalibrationProjector
+
+asyncio.run(ParkCalibrationProjector.amain())
diff --git a/python/lsst/ts/externalscripts/maintel/__init__.py b/python/lsst/ts/externalscripts/maintel/__init__.py
index 548abdaa..6f14999f 100644
--- a/python/lsst/ts/externalscripts/maintel/__init__.py
+++ b/python/lsst/ts/externalscripts/maintel/__init__.py
@@ -22,6 +22,7 @@
 from .make_comcam_calibrations import *
 from .parameter_march_comcam import *
 from .parameter_march_lsstcam import *
+from .park_calibration_projector import *
 from .take_comcam_guider_image import *
 from .take_ptc_flats_comcam import *
 from .take_rotated_comcam import *
diff --git a/python/lsst/ts/externalscripts/maintel/park_calibration_projector.py b/python/lsst/ts/externalscripts/maintel/park_calibration_projector.py
index d5992915..565dcc47 100644
--- a/python/lsst/ts/externalscripts/maintel/park_calibration_projector.py
+++ b/python/lsst/ts/externalscripts/maintel/park_calibration_projector.py
@@ -21,7 +21,7 @@
 
 __all__ = ["ParkCalibrationProjector"]
 
-
+import yaml
 from lsst.ts import salobj
 from lsst.ts.observatory.control.maintel.mtcalsys import MTCalsys
 
@@ -46,6 +46,21 @@ def __init__(self, index):
     def set_metadata(self, metadata):
         metadata.duration = 30
 
+    @classmethod
+    def get_schema(cls):
+        schema_yaml = """
+            $schema: http://json-schema.org/draft-07/schema#
+            $id: https://github.com/lsst-ts/ts_externalscripts/maintel/calibrations/park_calibration_projector.yaml # noqa: E501
+            title: SetupWhiteFlats v1
+            description: Configuration for SetupWhiteFlats.
+              Each attribute can be specified as a scalar or array.
+              All arrays must have the same length (one item per image).
+            type: object
+
+            additionalProperties: false
+        """
+        return yaml.safe_load(schema_yaml)
+
     async def configure(self, config):
         """Configure the script.
 
@@ -95,7 +110,8 @@ async def assert_components_enabled(self):
             summary_state = await comp.evt_summaryState.aget()
             try:
                 summaryState = summary_state.summaryState
-            except NameError:
+            except Exception as e:
+                self.log.debug(f"Exception: {e}")
                 summaryState = summary_state
             if salobj.State(summaryState) != salobj.State(salobj.State.ENABLED):
                 raise RuntimeError(f"{comp} is not ENABLED")
diff --git a/tests/maintel/test_park_calibration_projector.py b/tests/maintel/test_park_calibration_projector.py
index 63c9bb9a..5bc42d0f 100644
--- a/tests/maintel/test_park_calibration_projector.py
+++ b/tests/maintel/test_park_calibration_projector.py
@@ -49,6 +49,8 @@ async def basic_make_script(self, index):
         self.log.debug("Starting basic_make script")
         self.script = ParkCalibrationProjector(index=index)
 
+        await self.mock_calsys()
+
         self.log.debug("Finished initializing from basic_make_script")
         return (self.script,)
 
@@ -83,13 +85,13 @@ async def mock_calsys(self):
         self.script.led_projector.evt_summaryState.summaryState = (
             unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
         )
-        self.script.mtcalsys.tunablelaser = unittest.mock.AsyncMock()
-        self.script.mtcalsys.tunablelaser.evt_summaryState.aget = (
-            unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
-        )
-        self.script.mtcalsys.tunablelaser.evt_summaryState.summaryState = (
-            unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
-        )
+        # self.script.mtcalsys.tunablelaser = unittest.mock.AsyncMock()
+        # self.script.mtcalsys.tunablelaser.evt_summaryState.aget = (
+        #     unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
+        # )
+        # self.script.mtcalsys.tunablelaser.evt_summaryState.summaryState = (
+        #     unittest.mock.AsyncMock(return_value=salobj.State.ENABLED)
+        # )
 
     async def test_configure(self):
         async with self.make_script():
@@ -102,7 +104,6 @@ async def test_run_without_failures(self):
             assert self.script.state.state == Script.ScriptState.CONFIGURED
 
             self.log.debug("Starting Mtcalsys mocks")
-            await self.mock_mtcalsys()
             await self.mock_calsys()
 
             self.log.debug("Enable all CSCs")