Repository: QuTech-Delft/QMI (language: python)

Issue:
Add Santec TSL-570 laser driver
Description 
=========== 
A new driver needs to be created for Santec tunable laser model TSL-570. The controller can be connected through GPIB, USB (on Windows with a USB-to-serial driver installation) and LAN interfaces (TCP-IP). The command protocol is SCPI with carriage return "CR" as delimiter. With QMI and Linux, all connection options should in principle be viable through TCP and USBTMC transport interfaces. With Windows, GPIB is not (yet) developed, but with USB-to-serial driver installation serial transport works, and TCP of course as well.

As the hardware is using SCPI protocol, there is no need to refer to any manufacturer-provided software or libraries, thus there will be no license issues. The driver can be implemented also in the open-source QMI in Github.

Functionality / commands to be implemented:
- basic functions:
  - `*IDN`
  - `*RST`
  - `*OPC?`
  - `*CLS`
- control functions (get AND set where possible):
  - `:WAVelength`
  - `:WAVelength:UNIT`
  - `:WAVelength:FINe`
  - `:WAVelength:FINetuning:DISable`
  - `:COHCtrl`
  - `:POWer:STATe`
  - `:POWer`
  - `:POWer:ACTual?`
- From the sweep functions we will need the following:
  - `:WAVelength:SWEep:STARt`
  - `:WAVelength:SWEep:STOP`
  - `:WAVelength:SWEep:MODe`
  - `:WAVelength:SWEep:SPEed`
  - `:WAVelength:SWEep:CYCLes`
  - `:WAVelength:SWEep:STATe`
  - `:READout:DATa?`
- system functions:
  - `:SYSTem:ERRor?`
  - `:SYSTem:ALERt?`
  - `:SPECial:SHUTdown`
  - `:SPECial:REBoot`

These commands should allow the user to create sweeps in a script with some `for` loops. Optionally, if the sweeps do not seem to be satisfactorily implemented through a script, the controller has several SCPI commands for wavelength and frequency-based sweeps. 

Other possibly interesting commands might be setting an offset to the wavelength and trigger commands. The modulation enable/disable and modulation source might also be of interest.

Affected components 
------------------- 
qmi / QMI

Modules to be created 
--------------------- 
- `qmi/instruments/santec/__init__.py`
- `qmi/instruments/santec/tsl_570.py`

Tests to be created/updated 
---------------------------
- `tests/instruments/santec/test_tsl_570.py`
- hardware test for frequency sweep, power sweep?

Hardware 
-------- 
Santec laser controller.

Documentation to be updated 
--------------------------- 
- `CHANGELOG.md`. 
- _in-code documentation and comments_


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d8bade1..4d0c6290 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## \[x.y.z] - Unreleased
 
 ### Added
+- New QMI driver for Santec TSL-570 laser.
 - New QMI driver for Newport SMC100PP and new actuator UTS100PP in the `actuators.py`. 
 - New functions in the Newport_Single_Axis_Motion_controller and Newport_SMC100CC classes.
 
diff --git a/qmi/instruments/santec/__init__.py b/qmi/instruments/santec/__init__.py
new file mode 100644
index 00000000..2c752551
--- /dev/null
+++ b/qmi/instruments/santec/__init__.py
@@ -0,0 +1,8 @@
+"""
+Santec optical instruments.
+
+The qmi.instruments.santec package provides support for:
+- TSL-570 laser controller.
+"""
+
+from qmi.instruments.santec.tsl_570 import Santec_Tsl570
diff --git a/qmi/instruments/santec/tsl_570.py b/qmi/instruments/santec/tsl_570.py
new file mode 100644
index 00000000..c252367e
--- /dev/null
+++ b/qmi/instruments/santec/tsl_570.py
@@ -0,0 +1,917 @@
+"""
+Instrument driver for the Santec TSL 570 laser.
+"""
+
+import logging
+from dataclasses import dataclass
+from typing import List, Union
+
+from qmi.core.context import QMI_Context
+from qmi.core.exceptions import QMI_InstrumentException
+from qmi.core.instrument import QMI_Instrument, QMI_InstrumentIdentification
+from qmi.core.rpc import rpc_method
+from qmi.core.scpi_protocol import ScpiProtocol
+from qmi.core.transport import create_transport
+
+# Global variable holding the logger for this module.
+_logger = logging.getLogger(__name__)
+
+
+@dataclass
+class _WavelengthRange:
+    """Dataclass for wavelength instrument range."""
+
+    min: float
+    max: float
+
+
+@dataclass
+class _FrequencyRange:
+    """Dataclass for frequency instrument range."""
+
+    min: float
+    max: float
+
+
+@dataclass
+class _PowerLevelRange:
+    """Dataclass for power level instrument range."""
+
+    min: float
+    max: float
+
+
+class Santec_Tsl570(QMI_Instrument):
+    """Instrument driver for the Santec TSL 570 laser.
+
+    The driver is currently based on running at "Legacy" communication, which is also based on
+    SCPI protocol, but perhaps not fully complying to it.
+    """
+
+    # Default response timeout in seconds.
+    DEFAULT_RESPONSE_TIMEOUT = 5.0
+
+    # power level range in dBm and mW
+    POWER_LEVEL_RANGE = {"dBm": (-15.0, 13.0), "mW": (0.04, 20.00)}
+
+    # Valid speeds
+    VALID_SPEED_VALUES = [1, 2, 5, 10, 20, 50, 100, 200]
+
+    # System errors
+    ERRORS_TABLE = {
+        0: "No error",
+        -102: "Syntax error",
+        -103: "Invalid separator",
+        -108: "Parameter not allowed",
+        -109: "Missing parameter",
+        -113: "Undefined header",
+        -148: "Character data not allowed",
+        -200: "Execution error",
+        -222: "Data out of range",
+        -410: "Query INTERRUPTED",
+    }
+
+    # System alerts
+    ALERTS_TABLE = {
+        "No00.": "Power supply Error1",
+        "No02.": "Power supply Error2",
+        "No03.": "Power supply Error3",
+        "No05.": "Wavelength Error",
+        "No06.": "Power setting Error",
+        "No07.": "Inter lock detection",
+        "No20.": "Temperature control Error1",
+        "No21.": "Temperature control Error2",
+        "No22.": "Temperature control Error3",
+        "No23.": "Ongoing Warm up",
+        "No25.": "Shutter Error",
+        "No26.": "Sensor Error",
+        "No27.": "Connection Error",
+        "No28.": "Exhaust Fan Error",
+    }
+
+    def __init__(self, context: QMI_Context, name: str, transport: str) -> None:
+        """Initialize the instrument driver.
+
+        Parameters:
+            context:    The QMI context for running the driver in.
+            name:       Name for this instrument instance.
+            transport:  QMI transport descriptor to connect to the instrument.
+        """
+        super().__init__(context, name)
+        self._transport = create_transport(transport)
+        self._scpi_protocol = ScpiProtocol(
+            self._transport,
+            command_terminator="\r",
+            response_terminator="\r",
+            default_timeout=self.DEFAULT_RESPONSE_TIMEOUT,
+        )
+        # Instrument ranges for values
+        self._wavelength_range = _WavelengthRange
+        self._frequency_range = _FrequencyRange
+        self._power_level_range = _PowerLevelRange
+
+    def _check_error(self) -> List[str]:
+        """Read the instrument error queue and raise an exception if there is an error.
+
+        Returns:
+            errors + alerts: Listed error and alert response strings.
+        """
+        errors = []
+        # When there are no errors, the response is '0,"No error"'.
+        while (error := self._scpi_protocol.ask(":SYST:ERR?")) != '0,"No error"':
+            errors.append(self.ERRORS_TABLE[int(error.split(",")[0])])
+
+        alerts = []
+        # When there are no alerts, the response is "No alerts.".
+        while (alert := self._scpi_protocol.ask(":SYST:ALER?")) != "No alerts.":
+            alerts.append(self.ALERTS_TABLE[alert.split(",")[0]])
+
+        return errors + alerts
+
+    def _write_and_check_errors(self, cmd: str) -> None:
+        """Write a command to the instrument and check if any errors or alerts were raised.
+
+        Parameters:
+            cmd: The input command to send.
+
+        Raises:
+            QMI_InstrumentException: If there were errors, errors described in the exception message.
+        """
+        self._scpi_protocol.write(cmd)
+        errors = self._check_error()
+        if errors:
+            _logger.error("[%s] command [%s] resulted in errors: %s", self._name, cmd, errors)
+            raise QMI_InstrumentException(f"Command {cmd} resulted in errors: {errors}")
+
+    def _ask_int(self, cmd: str) -> int:
+        """Send a query and return a floating point response."""
+        resp = self._scpi_protocol.ask(cmd)
+        try:
+            return int(resp)
+        except ValueError as exc:
+            raise QMI_InstrumentException(f"Unexpected response to command {cmd!r}: {resp!r}") from exc
+
+    def _ask_float(self, cmd: str) -> float:
+        """Send a query and return a floating point response."""
+        resp = self._scpi_protocol.ask(cmd)
+        try:
+            return float(resp)
+        except ValueError as exc:
+            raise QMI_InstrumentException(f"Unexpected response to command {cmd!r}: {resp!r}") from exc
+
+    def _ask_bool(self, cmd: str) -> bool:
+        """Send a query and return a boolean response."""
+        resp = self._scpi_protocol.ask(cmd)
+        value = resp.strip().upper()
+        if value in ("1", "ON"):
+            return True
+        elif value in ("0", "OFF"):
+            return False
+        else:
+            raise QMI_InstrumentException(f"Unexpected response to command {cmd!r}: {resp!r}")
+
+    def _set_wavelength_unit(self, terahertz: int) -> None:
+        """Set the wavelength unit to terahertz or to nanometers.
+
+        Parameters:
+            terahertz: Integer to set wavelength unit to terahertz (1) or to nanometer (0)
+        """
+        self._write_and_check_errors(f":WAV:UNIT {terahertz}")
+
+    def _set_power_level_unit(self, milliwatts: int) -> None:
+        """Set the power_level unit to milliwatts or to decibel-milliwatts. Update the minimum and maximum values
+        consequently.
+
+        Parameters:
+            milliwatts: Integer to set power level unit to milliwatts (1) or to decibel-milliwatts (0)
+        """
+        self._write_and_check_errors(f":POW:UNIT {milliwatts}")
+        # Then update the minimum and maximum values of the instruments to apply for the changed units.
+        unit = "mW" if milliwatts else "dBm"
+        self._power_level_range.min = self.POWER_LEVEL_RANGE[unit][0]
+        self._power_level_range.max = self.POWER_LEVEL_RANGE[unit][1]
+
+    @rpc_method
+    def open(self) -> None:
+        _logger.info("[%s] Opening connection to instrument", self._name)
+        self._check_is_closed()
+        self._transport.open()
+        super().open()
+        # Then update the minimum and maximum values of the instrument ranges
+        self._wavelength_range.min = self.get_minimum_wavelength()
+        self._wavelength_range.max = self.get_maximum_wavelength()
+        self._frequency_range.min = self.get_minimum_frequency()
+        self._frequency_range.max = self.get_maximum_frequency()
+        power_unit = self.get_power_level_unit()
+        self._power_level_range.min = self.POWER_LEVEL_RANGE[power_unit][0]
+        self._power_level_range.max = self.POWER_LEVEL_RANGE[power_unit][1]
+
+    @rpc_method
+    def close(self) -> None:
+        _logger.info("[%s] Closing connection to instrument", self._name)
+        self._check_is_open()
+        super().close()
+        self._transport.close()
+
+    @rpc_method
+    def get_idn(self) -> QMI_InstrumentIdentification:
+        """Read instrument info and return QMI_InstrumentIdentification instance.
+
+        Returns:
+            QMI_InstrumentIdentification: Data with e.g. idn.vendor = SANTEC, idn.model = TSL-570,
+            idn.serial = 21020001, idn.version = 0001.000.0001 (firmware version).
+        """
+        resp = self._scpi_protocol.ask("*IDN?")
+        words = resp.rstrip().split(",")
+        if len(words) != 4:
+            raise QMI_InstrumentException("Unexpected response to *IDN?, got {!r}".format(resp))
+        return QMI_InstrumentIdentification(
+            vendor=words[0].strip(), model=words[1].strip(), serial=words[2].strip(), version=words[3].strip()
+        )
+
+    @rpc_method
+    def reset(self) -> None:
+        """Device reset. Aborts standby operation and clears the command input and error queues."""
+        _logger.info("[%s] Resetting instrument", self._name)
+        self._write_and_check_errors("*RST")
+
+    @rpc_method
+    def clear(self) -> None:
+        """Clear Status. Clears all event registers and queues and reflects the summary in the Status Byte Register.
+        Clears the following items.
+        ・Status Byte Register
+        ・Standard Event Status Register
+        ・Error Queue
+        """
+        _logger.info("[%s] Clearing instrument event registers and queues", self._name)
+        self._write_and_check_errors("*CLS")
+
+    @rpc_method
+    def operation_complete(self) -> bool:
+        """Check if the previous operation is completed.
+
+        Returns:
+            response: True for yes, False for no.
+        """
+        response = self._ask_bool("*OPC?")
+        return response
+
+    @rpc_method
+    def get_errors(self) -> List[str]:
+        """Query all errors and alerts.
+
+        Returns:
+            errors_and_alerts: A list of all error and alert descriptions.
+        """
+        return self._check_error()
+
+    @rpc_method
+    def set_wavelength(self, wavelength: float) -> None:
+        """Set the output wavelength in nanometers.
+
+        Parameters:
+            wavelength: The target wavelength in nanometers up to 0.1 pm resolution.
+
+        Raises:
+            QMI_InstrumentException: If the wavelength is not in the instrument range.
+        """
+        unit = "nm"
+        dec = 4  # 0.1 pm resolution
+        if wavelength < self._wavelength_range.min or wavelength > self._wavelength_range.max:
+            raise ValueError(
+                f"Wavelength {wavelength:.{dec}f}{unit} out of instrument range "
+                f"({self._wavelength_range.min}{unit} - {self._wavelength_range.max}{unit})"
+            )
+
+        self._write_and_check_errors(f":WAV {wavelength:.{dec}f}")
+
+    @rpc_method
+    def get_wavelength(self) -> float:
+        """Get the output wavelength. Default unit is nanometers.
+
+        Returns:
+            wavelength: The instrument wavelength in nanometers.
+        """
+        wavelength = self._ask_float(":WAV?")
+        return wavelength
+
+    @rpc_method
+    def get_minimum_wavelength(self) -> float:
+        """Get the minimum wavelength.
+
+        Returns:
+            wavelength: The instrument wavelength minimum in nanometers.
+        """
+        wavelength = self._ask_float(":WAV:MIN?")
+        return wavelength
+
+    @rpc_method
+    def get_maximum_wavelength(self) -> float:
+        """Get the maximum wavelength.
+
+        Returns:
+            wavelength: The instrument wavelength maximum in nanometers.
+        """
+        wavelength = self._ask_float(":WAV:MAX?")
+        return wavelength
+
+    @rpc_method
+    def set_frequency(self, frequency: float) -> None:
+        """Set the output frequency in terahertz.
+
+        Parameters:
+            frequency: The target frequency in terahertz up to 10 MHz resolution.
+
+        Raises:
+            QMI_InstrumentException: If the frequency is not in the instrument range.
+        """
+        unit = "THz"
+        dec = 5  # 10 MHz resolution
+        if frequency < self._frequency_range.min or frequency > self._frequency_range.max:
+            raise ValueError(
+                f"frequency {frequency:.{dec}f}{unit} out of instrument range "
+                f"({self._frequency_range.min}{unit} - {self._frequency_range.max}{unit})"
+            )
+
+        self._write_and_check_errors(f":WAV:FREQ {frequency:.{dec}f}")
+
+    @rpc_method
+    def get_frequency(self) -> float:
+        """Get the output frequency. Unit depends on unit setting.
+
+        Returns:
+            frequency: The instrument frequency in terahertz.
+        """
+        frequency = self._ask_float(":WAV:FREQ?")
+        return frequency
+
+    @rpc_method
+    def get_minimum_frequency(self) -> float:
+        """Get the minimum frequency.
+
+        Returns:
+            frequency: The instrument frequency minimum in terahertz.
+        """
+        frequency = self._ask_float(":WAV:FREQ:MIN?")
+        return frequency
+
+    @rpc_method
+    def get_maximum_frequency(self) -> float:
+        """Get the maximum frequency.
+
+        Returns:
+            frequency: The instrument frequency maximum in terahertz.
+        """
+        frequency = self._ask_float(":WAV:FREQ:MAX?")
+        return frequency
+
+    @rpc_method
+    def set_wavelength_unit_to_nm(self) -> None:
+        """Set the wavelength unit to nanometers."""
+        self._set_wavelength_unit(0)
+
+    @rpc_method
+    def set_wavelength_unit_to_thz(self) -> None:
+        """Set the wavelength unit to terahertz."""
+        self._set_wavelength_unit(1)
+
+    @rpc_method
+    def get_wavelength_unit(self) -> str:
+        """Get the wavelength unit displayed.
+
+        Returns:
+            wl_unit: The wavelength unit as either "THz" or "nm".
+        """
+        wl_unit = self._ask_bool(":WAV:UNIT?")
+        return "THz" if wl_unit else "nm"
+
+    @rpc_method
+    def set_wavelength_fine(self, value: float) -> None:
+        """Set the wavelength fine-tuning value.
+
+        Parameters:
+            value: The fine-tuning value in range -100.00 - +100.00 and maximal resolution of 0.01.
+
+        Raises:
+            ValueError: If value is not in the correct range.
+        """
+        if value < -100.0 or value > 100.0:
+            raise ValueError(f"Input value {value} not in valid range [-100...+100].")
+
+        rounded_value = round(value, 2)  # Step resolution is 0.01
+        self._write_and_check_errors(f":WAV:FIN {rounded_value:.2f}")
+
+    @rpc_method
+    def get_wavelength_fine(self) -> float:
+        """Get the wavelength fine-tuning value.
+
+        Returns:
+            value: The fine-tuning value in range -100.00 - +100.00 and maximal resolution of 0.01.
+        """
+        return self._ask_float("WAV:FIN?")
+
+    @rpc_method
+    def disable_finetuning_operation(self) -> None:
+        """Terminate Fine-Tuning operation."""
+        self._write_and_check_errors(":WAV:FIN:DIS")
+
+    @rpc_method
+    def set_coherence_control_status(self, status: Union[bool, str]) -> None:
+        """Set the Coherence control status.
+
+        Parameters:
+            status: New status as either True or "ON", or False or "OFF", to set control ON and OFF, respectively.
+
+        Raises:
+            AssertionError: If input parameter is a string but neither "ON" or "OFF".
+        """
+        if isinstance(status, bool):
+            i_status = int(status)
+
+        else:
+            assert status.upper() in ("ON", "OFF")
+            i_status = 0 if status.upper() == "OFF" else 1
+
+        self._write_and_check_errors(f":COHC {i_status}")
+
+    @rpc_method
+    def get_coherence_control_status(self) -> str:
+        """Get the Coherence control status.
+
+        Returns:
+            status: New status as either "ON", or "OFF".
+        """
+        status = self._ask_int(":COHC?")
+        return "ON" if status else "OFF"
+
+    @rpc_method
+    def set_optical_output_status(self, status: Union[bool, str]) -> None:
+        """Set the optical output status.
+
+        Parameters:
+            status: New status as either True or "ON", or False or "OFF", to set control ON and OFF, respectively.
+
+        Raises:
+            AssertionError: If input parameter is a string but neither "ON" or "OFF".
+        """
+        if isinstance(status, bool):
+            i_status = int(status)
+
+        else:
+            assert status.upper() in ("ON", "OFF")
+            i_status = 0 if status.upper() == "OFF" else 1
+
+        self._write_and_check_errors(f":POW:STAT {i_status}")
+
+    @rpc_method
+    def get_optical_output_status(self) -> str:
+        """Get the optical output status.
+
+        Returns:
+            status: New status as either "ON", or "OFF".
+        """
+        status = self._ask_int(":POW:STAT?")
+        return "ON" if status else "OFF"
+
+    @rpc_method
+    def set_power_level(self, power_level: float) -> None:
+        """Set optical output power level between -15dBm (~0.03mW) and peak power. Typical peak power from the
+        datasheet https://santec.imgix.net/TSL-570-Datasheet.pdf is 13dBm (~20.00mW).
+
+        Parameters:
+            power_level: power level in decibel-milliwatts or milliwatts.
+        """
+        unit = self.get_power_level_unit()
+        if power_level < self._power_level_range.min or power_level > self._power_level_range.max:
+            raise ValueError(
+                f"Power level {power_level:.2f}{unit} out of instrument range "
+                f"({self._power_level_range.min}{unit} - {self._power_level_range.max}{unit})"
+            )
+
+        self._write_and_check_errors(f":POW {power_level:.2f}")
+
+    @rpc_method
+    def get_power_level(self) -> float:
+        """Get the output power level setting. Unit depends on unit setting.
+
+        Returns:
+            power_level: The instrument power level in decibel-milliwatts or milliwatts.
+        """
+        power_level = self._ask_float(":POW?")
+        return round(power_level, 2)
+
+    @rpc_method
+    def get_actual_power(self) -> float:
+        """Get the monitored optical power. Unit depends on unit setting.
+
+        Returns:
+            power_level: The instrument power level in decibel-milliwatts or milliwatts.
+        """
+        power_level = self._ask_float(":POW:ACT?")
+        return round(power_level, 2)
+
+    @rpc_method
+    def get_minimum_power_level(self) -> float:
+        """Get the minimum power level.
+
+        Returns:
+            power level: The instrument power level minimum in decibel-milliwatts or milliwatts.
+        """
+        return round(self._power_level_range.min, 2)
+
+    @rpc_method
+    def get_maximum_power_level(self) -> float:
+        """Get the maximum power level.
+
+        Returns:
+            power level: The instrument power level maximum in decibel-milliwatts or milliwatts.
+        """
+        return round(self._power_level_range.max, 2)
+
+    @rpc_method
+    def set_power_level_unit_to_mw(self) -> None:
+        """Set the power_level unit to milliwatts."""
+        self._set_power_level_unit(1)
+
+    @rpc_method
+    def set_power_level_unit_to_dbm(self) -> None:
+        """Set the power_level unit to decibel-milliwatts."""
+        self._set_power_level_unit(0)
+
+    @rpc_method
+    def get_power_level_unit(self) -> str:
+        """Get the power_level unit displayed.
+
+        Returns:
+            wl_unit: The power_level unit as either "dBm" or "mW".
+        """
+        wl_unit = self._ask_bool(":POW:UNIT?")
+        return "mW" if wl_unit else "dBm"
+
+    @rpc_method
+    def set_sweep_start_wavelength(self, wavelength: float) -> None:
+        """Set the start wavelength for a sweep.
+
+        Parameters:
+            wavelength: wavelength in nanometers.
+        """
+        unit = "nm"
+        dec = 4  # 0.1 pm resolution
+        if wavelength < self._wavelength_range.min or wavelength > self._wavelength_range.max:
+            raise ValueError(
+                f"Wavelength {wavelength:.{dec}f}{unit} out of instrument range "
+                f"({self._wavelength_range.min}{unit} - {self._wavelength_range.max}{unit})"
+            )
+
+        self._write_and_check_errors(f":WAV:SWE:STAR {wavelength}")
+
+    @rpc_method
+    def get_sweep_start_wavelength(self) -> float:
+        """Get the sweep start wavelength. Default unit is nanometers.
+
+        Returns:
+            wavelength: The instrument wavelength in nanometers.
+        """
+        wavelength = self._ask_float(":WAV:SWE:STAR?")
+        return wavelength
+
+    @rpc_method
+    def set_sweep_stop_wavelength(self, wavelength: float) -> None:
+        """Set the stop wavelength for a sweep.
+
+        Parameters:
+            wavelength: wavelength in nanometers.
+        """
+        unit = "nm"
+        dec = 4  # 0.1 pm resolution
+        if wavelength < self._wavelength_range.min or wavelength > self._wavelength_range.max:
+            raise ValueError(
+                f"Wavelength {wavelength:.{dec}f}{unit} out of instrument range "
+                f"({self._wavelength_range.min}{unit} - {self._wavelength_range.max}{unit})"
+            )
+
+        self._write_and_check_errors(f":WAV:SWE:STOP {wavelength}")
+
+    @rpc_method
+    def get_sweep_stop_wavelength(self) -> float:
+        """Get the sweep stop wavelength. Default unit is nanometers.
+
+        Returns:
+            wavelength: The instrument wavelength in nanometers.
+        """
+        wavelength = self._ask_float(":WAV:SWE:STOP?")
+        return wavelength
+
+    @rpc_method
+    def set_sweep_start_frequency(self, frequency: float) -> None:
+        """Set the start frequency for a sweep.
+
+        Parameters:
+            frequency: frequency in terahertz.
+        """
+        unit = "THz"
+        dec = 4  # 10 MHz resolution
+        if frequency < self._frequency_range.min or frequency > self._frequency_range.max:
+            raise ValueError(
+                f"Wavelength {frequency:.{dec}f}{unit} out of instrument range "
+                f"({self._frequency_range.min}{unit} - {self._frequency_range.max}{unit})"
+            )
+
+        self._write_and_check_errors(f":WAV:FREQ:SWE:STAR {frequency}")
+
+    @rpc_method
+    def get_sweep_start_frequency(self) -> float:
+        """Get the sweep start frequency. Default unit is terahertz.
+
+        Returns:
+            frequency: The instrument frequency in nanometers.
+        """
+        frequency = self._ask_float(":WAV:FREQ:SWE:STAR?")
+        return frequency
+
+    @rpc_method
+    def set_sweep_stop_frequency(self, frequency: float) -> None:
+        """Set the stop frequency for a sweep.
+
+        Parameters:
+            frequency: frequency in terahertz.
+        """
+        unit = "THz"
+        dec = 4  # 10 MHz resolution
+        if frequency < self._frequency_range.min or frequency > self._frequency_range.max:
+            raise ValueError(
+                f"Wavelength {frequency:.{dec}f}{unit} out of instrument range "
+                f"({self._frequency_range.min}{unit} - {self._frequency_range.max}{unit})"
+            )
+
+        self._write_and_check_errors(f":WAV:FREQ:SWE:STOP {frequency}")
+
+    @rpc_method
+    def get_sweep_stop_frequency(self) -> float:
+        """Get the sweep stop frequency. Default unit is terahertz.
+
+        Returns:
+            frequency: The instrument frequency in nanometers.
+        """
+        frequency = self._ask_float(":WAV:FREQ:SWE:STOP?")
+        return frequency
+
+    @rpc_method
+    def set_sweep_mode(self, mode: int) -> None:
+        """Set sweep mode. Possible modes are:
+        0 - Step sweep mode and One way
+        1 - Continuous sweep mode and One way
+        2 - Step sweep mode and Two way
+        3 - Continuous sweep mode and Two way
+
+        Parameters:
+            mode: Integer in range [0, 3].
+        """
+        if mode not in range(4):
+            raise ValueError(f"Sweep mode value {mode} not in valid modes range [0-3].")
+
+        self._write_and_check_errors(f":WAV:SWE:MOD {mode}")
+
+    @rpc_method
+    def get_sweep_mode(self) -> int:
+        """Get sweep mode.
+
+        Returns:
+            mode: Integer in range [0, 3].
+        """
+        return self._ask_int(":WAV:SWE:MOD?")
+
+    @rpc_method
+    def set_sweep_speed(self, speed: int) -> None:
+        """Set sweep speed. Possible speed values are 1,2,5,10,20,50,100,200nm/s.
+
+        Parameters:
+            speed: speed in nm/s.
+        """
+        if speed not in self.VALID_SPEED_VALUES:
+            raise ValueError(f"Sweep speed {speed} not in valid values {self.VALID_SPEED_VALUES}nm/s.")
+
+        self._write_and_check_errors(f":WAV:SWE:SPE {speed}")
+
+    @rpc_method
+    def get_sweep_speed(self) -> float:
+        """Get sweep speed.
+
+        Returns:
+            speed: Speed in range [1, 200]nm/s.
+        """
+        return self._ask_float(":WAV:SWE:SPE?")
+
+    @rpc_method
+    def set_sweep_dwell(self, dwell: float) -> None:
+        """Set _stepped_ sweep dwell. Possible dwell range is 0-999.9s in 0.1nm steps. It does not take into account
+        delay in one-way sweeps to return to the start frequency.
+
+        Parameters:
+            dwell: Dwell between sweep steps in range [0, 1000[s.
+        """
+        dec = 1
+        if dwell < 0.0 or dwell >= 1000.0:
+            raise ValueError(f"Sweep dwell {dwell} not in valid range [0-1000[s.")
+
+        self._write_and_check_errors(f":WAV:SWE:DWEL {dwell:.{dec}f}")
+
+    @rpc_method
+    def get_sweep_dwell(self) -> float:
+        """Get dwell between sweep steps.
+
+        Returns:
+            dwell: dwell in range [0, 1000[s.
+        """
+        return self._ask_float(":WAV:SWE:DWEL?")
+
+    @rpc_method
+    def set_sweep_delay(self, delay: float) -> None:
+        """Set _continuous_ sweep delay. Possible delay range is 0-999.9s in 0.1nm steps. It does not take into account
+        delay in one-way sweeps to return to the start frequency.
+
+        Parameters:
+            delay: Delay between sweeps in range [0, 1000[s.
+        """
+        dec = 1
+        if delay < 0.0 or delay >= 1000.0:
+            raise ValueError(f"Sweep delay {delay} not in valid range [0-1000[s.")
+
+        self._write_and_check_errors(f":WAV:SWE:DEL {delay:.{dec}f}")
+
+    @rpc_method
+    def get_sweep_delay(self) -> float:
+        """Get delay between sweeps.
+
+        Returns:
+            delay: delay in range [0, 1000[s.
+        """
+        return self._ask_float(":WAV:SWE:DEL?")
+
+    @rpc_method
+    def set_sweep_cycles(self, cycles: int) -> None:
+        """Set sweep repetition times. Possible number of repetitions are in range [0, 999].
+
+        Parameters:
+            cycles: Integer in range [0, 999].
+        """
+        if cycles not in range(1000):
+            raise ValueError(f"Sweep cycles {cycles} not in valid cycles range [0-999].")
+
+        self._write_and_check_errors(f":WAV:SWE:CYCL {cycles}")
+
+    @rpc_method
+    def get_sweep_cycles(self) -> int:
+        """Get sweep repetition times.
+
+        Returns:
+            cycles: Integer in range [0, 999].
+        """
+        return self._ask_int(":WAV:SWE:CYCL?")
+
+    @rpc_method
+    def set_sweep_state(self, state: int) -> None:
+        """Set sweep state. Possible states are:
+        0 - Stop
+        1 - Start
+
+        Parameters:
+            state: Integer in range [0, 1].
+        """
+        if state not in [0, 1]:
+            raise ValueError(f"Sweep state value {state} not in valid states range [0-1].")
+
+        self._write_and_check_errors(f":WAV:SWE:STAT {state}")
+
+    @rpc_method
+    def start_repeating_sweep(self) -> None:
+        """Start a repeating sweep."""
+        self._write_and_check_errors(":WAV:SWE:STAT:REP")
+
+    @rpc_method
+    def get_sweep_state(self) -> int:
+        """Get sweep state. Possible states are:
+        0 - Stopped
+        1 - Running
+        3 - Standing by trigger
+        4 - Preparation for sweep start
+
+        Returns:
+            state: Integer in range [0, 4].
+        """
+        return self._ask_int(":WAV:SWE:STAT?")
+
+    @rpc_method
+    def set_trigger_output_timing_mode(self, mode: int) -> None:
+        """Set trigger output timing mode. Possible modes are:
+        0 - None
+        1 - Stop
+        2 - Start
+        3 - Step
+
+        Parameters:
+            mode: Integer in range [0, 3].
+        """
+        if mode not in range(4):
+            raise ValueError(f"Trigger output timing mode {mode} not in valid modes range [0-3].")
+
+        self._write_and_check_errors(f":TRIG:OUTP {mode}")
+
+    @rpc_method
+    def get_trigger_output_timing_mode(self) -> int:
+        """Get trigger output timing mode.
+
+        Returns:
+            mode: Integer in range [0, 3].
+        """
+        return self._ask_int(":TRIG:OUTP?")
+
+    @rpc_method
+    def set_trigger_output_period_mode(self, mode: int) -> None:
+        """Set trigger output period mode. Possible modes are:
+        0 - Sets the output trigger to be periodic in time.
+        1 - Sets the output trigger to be periodic in wavelength.
+
+        Parameters:
+            mode: Integer in range [0, 1].
+        """
+        if mode not in [0, 1]:
+            raise ValueError(f"Trigger output period mode {mode} not in valid modes [0-1].")
+
+        self._write_and_check_errors(f":TRIG:OUTP:SETT {mode}")
+
+    @rpc_method
+    def get_trigger_output_period_mode(self) -> int:
+        """Get trigger output period mode.
+
+        Returns:
+            mode: Integer in range [0, 1].
+        """
+        return self._ask_int(":TRIG:OUTP:SETT?")
+
+    @rpc_method
+    def set_trigger_output_step(self, step: float) -> None:
+        """Set trigger output step. Possible step range is from 0.0001 to ~ maximum specified wavelength
+        range in 0.0001nm steps.
+
+        Parameters:
+            step: Interval of the trigger signal output [0.0001, max wavelength] in nanometers.
+        """
+        dec = 4
+        max_wl_range = self.get_sweep_stop_wavelength() - self.get_sweep_start_wavelength()
+        if step < 0.0001 or step > max_wl_range:
+            raise ValueError(f"Trigger output step {step} not in valid range [0.0001-{max_wl_range}]nm.")
+
+        self._write_and_check_errors(f":TRIG:OUTP:STEP {step:.{dec}f}")
+
+    @rpc_method
+    def get_trigger_output_step(self) -> float:
+        """Get trigger output step.
+
+        Returns:
+            step: step in range [0.0001, max wavelength]nm.
+        """
+        return self._ask_float(":TRIG:OUTP:STEP?")
+
+    @rpc_method
+    def issue_soft_trigger(self) -> None:
+        """Issues a soft trigger, executing sweep from trigger standby mode."""
+        self._write_and_check_errors(":TRIG:INP:SOFT")
+
+    @rpc_method
+    def readout_points(self) -> int:
+        """reads the number of logging data available.
+
+        Returns:
+            The number of logging data available.
+        """
+        return int(self._scpi_protocol.ask(":READ:POIN?"))
+
+    @rpc_method
+    def readout_data(self) -> List[float]:
+        """Read out wavelength logging data and convert it into floating point values. According to the manual
+        the data points are returned in units of 0.1pm. Thus, value 0x0040F844 (little Endian order) = 4520000
+        corresponds to 452.0000nm.
+
+        Returns:
+            data: Data points list converted into nanometers.
+        """
+        data_binary_size = 4  # In Legacy mode
+        self._scpi_protocol.write(":READout:DATa?")
+        binary_data = self._scpi_protocol.read_binary_data(read_terminator_flag=False)
+        hex_data = [binary_data[p : p + data_binary_size] for p in range(0, len(binary_data), data_binary_size)]
+
+        return [int.from_bytes(h_d, byteorder="little") * 1e-4 for h_d in hex_data]
+
+    @rpc_method
+    def shutdown(self) -> None:
+        """Shut down the device. Breaks also communication with the device."""
+        self._write_and_check_errors(":SPEC:SHUT")
+
+    @rpc_method
+    def reboot(self) -> None:
+        """Restarts the device. This takes about 60 seconds. Note that the communication to the
+        device also resets, so continuing with the same proxy is not possible, but a new one needs
+        to be made.
+        """
+        self._write_and_check_errors(":SPECial:REBoot")


Test patch:
diff --git a/tests/instruments/santec/__init__.py b/tests/instruments/santec/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/instruments/santec/test_tsl_570.py b/tests/instruments/santec/test_tsl_570.py
new file mode 100644
index 00000000..704078c5
--- /dev/null
+++ b/tests/instruments/santec/test_tsl_570.py
@@ -0,0 +1,1498 @@
+"""Unit-tests for Santec TSL-570 driver class."""
+import logging
+import random
+import unittest
+from unittest.mock import call, patch, Mock
+import numpy as np
+
+import qmi
+from qmi.core.scpi_protocol import ScpiProtocol
+from qmi.core.exceptions import QMI_InstrumentException
+from qmi.core.transport import QMI_TcpTransport
+
+from qmi.instruments.santec.tsl_570 import Santec_Tsl570
+
+
+class TestSantecTsl570OpenClose(unittest.TestCase):
+
+    def setUp(self):
+        logging.getLogger("qmi.instruments.santec.tsl_570").setLevel(logging.CRITICAL)
+        qmi.start("TestSantecTsl570ClassContext")
+        # Add patches
+        patcher = patch('qmi.instruments.santec.tsl_570.create_transport', spec=QMI_TcpTransport)
+        self._transport_mock = patcher.start()
+        self.addCleanup(patcher.stop)
+        patcher2 = patch('qmi.instruments.santec.tsl_570.ScpiProtocol', autospec=ScpiProtocol)
+        self._scpi_mock = patcher2.start()
+        self.addCleanup(patcher2.stop)
+        self._scpi_mock.write = Mock()
+        # Make DUT
+        self.instr: Santec_Tsl570 = Santec_Tsl570(qmi.context(), "Santec_laser", "")
+
+    def tearDown(self):
+        qmi.stop()
+        logging.getLogger("qmi.instruments.santec.tsl_570").setLevel(logging.NOTSET)
+
+    def test_open_close(self):
+        """open() command not just opens the instrument, but immediately also checks the wavelength/frequency
+        range and the power unit to set the power level range. Test the whole shebang.
+
+        close() command then just closes the connection to the instrument and the transport.
+        """
+        # Initially, the wavelength and power range should be bare data classes
+        expected_initial_out = "<class 'qmi.instruments.santec.tsl_570._{rng}Range'>"
+        self.assertEqual(expected_initial_out.format(rng="Wavelength"), str(self.instr._wavelength_range))
+        self.assertEqual(expected_initial_out.format(rng="Frequency"), str(self.instr._frequency_range))
+        self.assertEqual(expected_initial_out.format(rng="PowerLevel"), str(self.instr._power_level_range))
+        # Arrange
+        expected_wl_min = 1200.0
+        expected_wl_max = 1700.0
+        expected_freq_min = 175.0
+        expected_freq_max = 250.0
+        power_unit = 0  # "dBm"
+        expected_power_min = -15.0
+        expected_power_max = 13.0
+
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{expected_wl_min}\r",
+            f"{expected_wl_max}\r",
+            f"{expected_freq_min}\r",
+            f"{expected_freq_max}\r",
+            f"{power_unit}\r"
+        ]
+        expected_calls = [
+            call().ask(":WAV:MIN?"),
+            call().ask(":WAV:MAX?"),
+            call().ask(":WAV:FREQ:MIN?"),
+            call().ask(":WAV:FREQ:MAX?"),
+            call().ask(":POW:UNIT?")
+        ]
+        # Act
+        self.instr.open()
+        # Assert
+        self._transport_mock.assert_called_once()
+        self.assertEqual(expected_wl_min, self.instr._wavelength_range.min)
+        self.assertEqual(expected_wl_max, self.instr._wavelength_range.max)
+        self.assertEqual(expected_power_min, self.instr._power_level_range.min)
+        self.assertEqual(expected_power_max, self.instr._power_level_range.max)
+        self._scpi_mock.assert_has_calls(expected_calls, any_order=True)
+        # Test close
+        self.instr.close()
+        self._transport_mock.assert_called_once()
+
+    def test_open_close_alternate_unit(self):
+        """Test the open() - close() but with in milliwatt units"""
+        # Arrange
+        expected_wl_min = 1200.0
+        expected_wl_max = 1700.0
+        expected_freq_min = 175.0
+        expected_freq_max = 250.0
+        power_unit = 1  # "mW"
+        expected_power_min = 0.04
+        expected_power_max = 20.0
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{expected_wl_min}\r",
+            f"{expected_wl_max}\r",
+            f"{expected_freq_min}\r",
+            f"{expected_freq_max}\r",
+            f"{power_unit}\r"
+        ]
+        expected_calls = [
+            call().ask(":WAV:MIN?"),
+            call().ask(":WAV:MAX?"),
+            call().ask(":WAV:FREQ:MIN?"),
+            call().ask(":WAV:FREQ:MAX?"),
+            call().ask(":POW:UNIT?")
+        ]
+        # Act
+        self.instr.open()
+        # Assert
+        self._transport_mock.assert_called_once()
+        self.assertEqual(expected_wl_min, self.instr._wavelength_range.min)
+        self.assertEqual(expected_wl_max, self.instr._wavelength_range.max)
+        self.assertEqual(expected_freq_min, self.instr._frequency_range.min)
+        self.assertEqual(expected_freq_max, self.instr._frequency_range.max)
+        self.assertEqual(expected_power_min, self.instr._power_level_range.min)
+        self.assertEqual(expected_power_max, self.instr._power_level_range.max)
+        self._scpi_mock.assert_has_calls(expected_calls)
+        # Test close
+        self.instr.close()
+        self._transport_mock.assert_called_once()
+
+
+class TestSantecTsl570ClassMethods(unittest.TestCase):
+
+    def setUp(self):
+        # Make as standard to open in wavelength and dBm mode.
+        self.wl_min = 1200.0
+        self.wl_max = 1700.0
+        self.freq_min = 175.0
+        self.freq_max = 250.0
+        power_unit = 0  # "dBm"
+        self.power_min = -15.0
+        self.power_max = 13.0
+
+        logging.getLogger("qmi.instruments.santec.tsl_570").setLevel(logging.CRITICAL)
+        qmi.start("TestSantecTsl570ClassContext")
+        # Add patches
+        patcher = patch('qmi.instruments.santec.tsl_570.create_transport', spec=QMI_TcpTransport)
+        self._transport_mock = patcher.start()
+        self.addCleanup(patcher.stop)
+        patcher2 = patch('qmi.instruments.santec.tsl_570.ScpiProtocol', autospec=ScpiProtocol)
+        self._scpi_mock = patcher2.start()
+        self.addCleanup(patcher2.stop)
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{self.wl_min}\r",
+            f"{self.wl_max}\r",
+            f"{self.freq_min}\r",
+            f"{self.freq_max}\r",
+            f"{power_unit}\r"
+        ]
+        self._scpi_mock.write = Mock()
+        # Make DUT
+        self.instr: Santec_Tsl570 = Santec_Tsl570(qmi.context(), "RSB100a", "")
+        self.instr.open()
+        # Reset the SCPI mock for further tests
+        self._scpi_mock.reset_mock()
+
+    def tearDown(self):
+        self.instr.close()
+        qmi.stop()
+        logging.getLogger("qmi.instruments.santec.tsl_570").setLevel(logging.NOTSET)
+
+    def test_wrong_int_value(self):
+        """Test a call that uses _ask_int to see that it raises an error at wrong response"""
+        self._scpi_mock(self._transport_mock).ask.side_effect = ["not an int"]
+
+        with self.assertRaises(QMI_InstrumentException):
+            self.instr.get_coherence_control_status()
+
+    def test_wrong_float_value(self):
+        """Test a call that uses _ask_float to see that it raises an error at wrong response"""
+        self._scpi_mock(self._transport_mock).ask.side_effect = ["not a float"]
+
+        with self.assertRaises(QMI_InstrumentException):
+            self.instr.get_wavelength()
+
+    def test_wrong_bool_value(self):
+        """Test a call that uses _ask_bool to see that it raises an error at wrong response"""
+        self._scpi_mock(self._transport_mock).ask.side_effect = ["not a bool"]
+
+        with self.assertRaises(QMI_InstrumentException):
+            self.instr.operation_complete()
+
+    def test_get_idn(self):
+        """Test ident. """
+        vendor = "vendor"
+        model = "model"
+        serial = "serial"
+        version = "version"
+        self._scpi_mock(self._transport_mock).ask.side_effect = [f"{vendor},{model},{serial},{version}"]
+
+        ident = self.instr.get_idn()
+
+        self._scpi_mock.assert_has_calls([call().ask("*IDN?")])
+        self.assertEqual(ident.vendor, vendor)
+        self.assertEqual(ident.model, model)
+        self.assertEqual(ident.serial, serial)
+        self.assertEqual(ident.version, version)
+
+    def test_wrong_idn_response(self):
+        """Test ident. raises QMI_InstrumentException by a non-sense response."""
+        self._scpi_mock(self._transport_mock).ask.side_effect = ["nonsense"]
+
+        with self.assertRaises(QMI_InstrumentException):
+            self.instr.get_idn()
+
+        self._scpi_mock.assert_has_calls([call().ask("*IDN?")])
+
+    def test_reset(self):
+        """Test reset."""
+        self._scpi_mock(self._transport_mock).ask.side_effect = ['0,"No error"', "No alerts."]
+
+        self.instr.reset()
+
+        self._scpi_mock.assert_has_calls([call().write("*RST")])
+        self._scpi_mock.assert_has_calls([call().ask(":SYST:ERR?")])
+        self._scpi_mock.assert_has_calls([call().ask(":SYST:ALER?")])
+
+    def test_clear(self):
+        """Test clear."""
+        self._scpi_mock(self._transport_mock).ask.side_effect = ['0,"No error"', "No alerts."]
+
+        self.instr.clear()
+
+        self._scpi_mock.assert_has_calls([call().write("*CLS")])
+        self._scpi_mock.assert_has_calls([call().ask(":SYST:ERR?")])
+        self._scpi_mock.assert_has_calls([call().ask(":SYST:ALER?")])
+
+    def test_operation_completed(self):
+        """Test getting OPC status."""
+        self._scpi_mock(self._transport_mock).ask.side_effect = ["1"]
+
+        opc = self.instr.operation_complete()
+
+        self.assertTrue(opc)
+        self._scpi_mock.assert_has_calls([call().ask("*OPC?")])
+
+    def test_error(self):
+        """Test command with error and alert responses. '0,"No error"' should be returned when no further
+        errors or alerts are present."""
+        # Arrange
+        error_code = -410
+        alert_code = "No27."
+        expected_error = [Santec_Tsl570.ERRORS_TABLE[error_code]]
+        expected_alert = [Santec_Tsl570.ALERTS_TABLE[alert_code]]
+        expected_exception = f"Command *RST resulted in errors: {expected_error + expected_alert}"
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{error_code}",
+            '0,"No error"',
+            f"{alert_code}",
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write("*RST")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        with self.assertRaises(QMI_InstrumentException) as exc:
+            self.instr.reset()
+        # Assert
+        self.assertEqual(expected_exception, str(exc.exception))
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_get_errors(self):
+        """Test the get_errors returns list of errors."""
+        error_code = -102
+        alert_code = "No07."
+        expected_error = [Santec_Tsl570.ERRORS_TABLE[error_code]]
+        expected_alert = [Santec_Tsl570.ALERTS_TABLE[alert_code]]
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{error_code}",
+            '0,"No error"',
+            f"{alert_code}",
+            "No alerts."
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        response = self.instr.get_errors()
+        # Assert
+        self.assertEqual(expected_error + expected_alert, response)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_wavelength_in_nm(self):
+        """Test setting the wavelength in nm."""
+        input_wl = 1450
+        expected_dec = 4  # resolution is 0.1pm, so we should use 4 decimals
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV {input_wl:.{expected_dec}f}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_wavelength(input_wl)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_wavelength_in_nm_excepts(self):
+        """Test setting the wavelength in nm with values out-of-bounds."""
+        input_wl = [self.wl_min - 1, self.wl_max + 1]
+        # Act
+        for inp in input_wl:
+            with self.assertRaises(ValueError):
+                self.instr.set_wavelength(inp)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_wavelength_in_nm(self):
+        """Test getting the wavelength in nm."""
+        wl = 1450.0
+        expected_wl = round(wl, 4)  # We use 4 decimals for rounding output wl.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{wl}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV?"),
+        ]
+        # Act
+        wavelength = self.instr.get_wavelength()
+        # Assert
+        self.assertEqual(expected_wl, wavelength)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_get_minimum_wavelength_in_nm(self):
+        """Test getting the minimum wavelength in nm."""
+        expected_wl = round(self.wl_min, 4)  # We use 4 decimals for rounding output wl.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{self.wl_min}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:MIN?"),
+        ]
+        # Act
+        wavelength = self.instr.get_minimum_wavelength()
+        # Assert
+        self.assertEqual(expected_wl, wavelength)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_get_maximum_wavelength_in_nm(self):
+        """Test getting the maximum wavelength in nm."""
+        expected_wl = round(self.wl_max, 4)  # We use 4 decimals for rounding output wl.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{self.wl_max}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:MAX?"),
+        ]
+        # Act
+        wavelength = self.instr.get_maximum_wavelength()
+        # Assert
+        self.assertEqual(expected_wl, wavelength)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_frequency_in_thz(self):
+        """Test setting the frequency in thz."""
+        input_freq = 231.0
+        expected_dec = 5  # resolution is 10MHz, so we should use 5 decimals
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV:FREQ {input_freq:.{expected_dec}f}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_frequency(input_freq)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_frequency_in_thz_excepts(self):
+        """Test setting the frequency in thz with values out-of-bounds."""
+        input_freq = [self.freq_min - 1, self.freq_max + 1]
+        # Act
+        for inp in input_freq:
+            with self.assertRaises(ValueError):
+                self.instr.set_frequency(inp)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_frequency_in_thz(self):
+        """Test getting the frequency in THz."""
+        freq = 200.0
+        expected_freq = round(freq, 5)  # We use 5 decimals for rounding output freq.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{freq}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:FREQ?"),
+        ]
+        # Act
+        frequency = self.instr.get_frequency()
+        # Assert
+        self.assertEqual(expected_freq, frequency)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_get_minimum_frequency_in_thz(self):
+        """Test getting the minimum frequency in THz."""
+        expected_freq = round(self.freq_min, 5)  # We use 5 decimals for rounding output freq.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{self.freq_min}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:FREQ:MIN?"),
+        ]
+        # Act
+        frequency = self.instr.get_minimum_frequency()
+        # Assert
+        self.assertEqual(expected_freq, frequency)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_get_maximum_frequency_in_thz(self):
+        """Test getting the maximum frequency in THz."""
+        expected_freq = round(self.freq_max, 5)  # We use 5 decimals for rounding output freq.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{self.freq_max}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:FREQ:MAX?"),
+        ]
+        # Act
+        frequency = self.instr.get_maximum_frequency()
+        # Assert
+        self.assertEqual(expected_freq, frequency)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_wavelength_unit(self):
+        """Test setting the wavelength unit."""
+        expected_units = [1, 0]  # is ["THz", "nm"]
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts.",
+            '0,"No error"',
+            "No alerts.",
+        ]
+        expected_calls = [
+            call().write(f":WAV:UNIT {expected_units[0]}"),
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?"),
+            call().write(f":WAV:UNIT {expected_units[1]}"),
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?"),
+        ]
+        # Act
+        self.instr.set_wavelength_unit_to_thz()
+        self.instr.set_wavelength_unit_to_nm()
+
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_calls)
+
+    def test_get_wavelength_unit_nm(self):
+        """Test getting the wavelength unit as nm."""
+        expected_unit = "nm"
+        self._scpi_mock(self._transport_mock).ask.side_effect = ["0"]
+        expected_ask_calls = [
+            call().ask(":WAV:UNIT?"),
+        ]
+        # Act
+        unit = self.instr.get_wavelength_unit()
+        # Assert
+        self.assertEqual(expected_unit, unit)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_get_wavelength_unit_thz(self):
+        """Test getting the wavelength unit as THz."""
+        expected_unit = "THz"
+        self._scpi_mock(self._transport_mock).ask.side_effect = ["1"]
+        expected_ask_calls = [
+            call().ask(":WAV:UNIT?"),
+        ]
+        # Act
+        unit = self.instr.get_wavelength_unit()
+        # Assert
+        self.assertEqual(expected_unit, unit)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_wavelength_fine(self):
+        """Test setting the wavelength fine-tuning value."""
+        input_wl_fine = 55.0
+        expected_dec = 2  # resolution is 0.01, so we should use 2 decimals
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV:FIN {input_wl_fine:.{expected_dec}f}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_wavelength_fine(input_wl_fine)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_wavelength_fine_excepts(self):
+        """Test setting the wavelength fine-tuning value out-of-bounds."""
+        input_wl_fine = [-100.1, 100.1]  # Range is [-100.0, 100.0]
+        # Act
+        for inp in input_wl_fine:
+            with self.assertRaises(ValueError):
+                self.instr.set_wavelength_fine(inp)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_wavelength_fine(self):
+        """Test getting the wavelength fine-tuning value."""
+        wl = 1450.0
+        expected_wl = round(wl, 5)  # We use 5 decimals for rounding output wl.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{wl}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV?"),
+        ]
+        # Act
+        wavelength = self.instr.get_wavelength()
+        # Assert
+        self.assertEqual(expected_wl, wavelength)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_disable_finetuning_operation(self):
+        """Test disabling the fine-tuning operation."""
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts.",
+        ]
+        expected_write_calls = [
+            call().write(":WAV:FIN:DIS"),
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.disable_finetuning_operation()
+
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls, any_order=True)
+        self._scpi_mock.assert_has_calls(expected_ask_calls, any_order=True)
+
+    def test_set_coherence_control_status(self):
+        """Test setting the coherence control_status."""
+        inputs = [True, "OFF"]
+        expected_control_status = [1, 0]  # is ["ON", "OFF"]
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts.",
+            '0,"No error"',
+            "No alerts.",
+        ]
+        expected_write_calls = [
+            call().write(f":COHC {expected_control_status[0]}"),
+            call().write(f":COHC {expected_control_status[1]}"),
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?"),
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?"),
+        ]
+        # Act
+        for e, inp in enumerate(inputs):
+            self.instr.set_coherence_control_status(inp)
+
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls, any_order=True)
+        self._scpi_mock.assert_has_calls(expected_ask_calls, any_order=True)
+
+    def test_set_coherence_control_status_excepts(self):
+        """See that the function excepts if invalid string is given as input."""
+        with self.assertRaises(AssertionError):
+            self.instr.set_coherence_control_status("AAN")
+
+    def test_get_coherence_control_status(self):
+        """Test getting the coherence control_status."""
+        expected_control_status = "OFF"
+        self._scpi_mock(self._transport_mock).ask.side_effect = ["0"]
+        expected_ask_calls = [
+            call().ask(":COHC?"),
+        ]
+        # Act
+        control_status = self.instr.get_coherence_control_status()
+        # Assert
+        self.assertEqual(expected_control_status, control_status)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_optical_output_status(self):
+        """Test setting the optical output_status."""
+        inputs = [True, "OFF"]
+        expected_output_status = [1, 0]  # is ["ON", "OFF"]
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts.",
+            '0,"No error"',
+            "No alerts.",
+        ]
+        expected_write_calls = [
+            call().write(f":POW:STAT {expected_output_status[0]}"),
+            call().write(f":POW:STAT {expected_output_status[1]}"),
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?"),
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?"),
+        ]
+        # Act
+        for e, inp in enumerate(inputs):
+            self.instr.set_optical_output_status(inp)
+
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls, any_order=True)
+        self._scpi_mock.assert_has_calls(expected_ask_calls, any_order=True)
+
+    def test_set_optical_output_status_excepts(self):
+        """See that the function excepts if invalid string is given as input."""
+        with self.assertRaises(AssertionError):
+            self.instr.set_optical_output_status("UIT")
+
+    def test_get_optical_output_status(self):
+        """Test getting the optical output_status."""
+        expected_output_status = "ON"
+        self._scpi_mock(self._transport_mock).ask.side_effect = ["1"]
+        expected_ask_calls = [
+            call().ask(":POW:STAT?"),
+        ]
+        # Act
+        output_status = self.instr.get_optical_output_status()
+        # Assert
+        self.assertEqual(expected_output_status, output_status)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_power_level_in_dbm(self):
+        """Test setting the power_level in dbm."""
+        input_pow = 2
+        expected_dec = 2  # resolution is 0.01dbm, so we should use 2 decimals
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            "0",
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":POW {input_pow:.{expected_dec}f}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_power_level(input_pow)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_power_level_in_dbm_excepts(self):
+        """Test setting the power_level in dbm with values out-of-bounds."""
+        input_pow = [self.power_min - 1, self.power_max + 1]
+        self._scpi_mock(self._transport_mock).ask.side_effect = ["0", "0"]
+        # Act
+        for inp in input_pow:
+            with self.assertRaises(ValueError):
+                self.instr.set_power_level(inp)
+
+        # Assert
+        self._scpi_mock.assert_has_calls([call().ask(":POW:UNIT?")])
+
+    def test_get_power_level_in_dbm(self):
+        """Test getting the power_level in dbm."""
+        pow = 9.0
+        expected_pow = round(pow, 2)  # We use 2 decimals for rounding output pow.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{pow}"
+        ]
+        expected_ask_calls = [
+            call().ask(":POW?"),
+        ]
+        # Act
+        power_level = self.instr.get_power_level()
+        # Assert
+        self.assertEqual(expected_pow, power_level)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_get_minimum_power_level_in_dbm(self):
+        """Test getting the minimum power_level in dbm."""
+        # Arrange
+        expected_pow = round(self.power_min, 2)  # We use 2 decimals for rounding output pow.
+        # Act
+        power_level = self.instr.get_minimum_power_level()
+        # Assert
+        self.assertEqual(expected_pow, power_level)
+        self._scpi_mock.assert_not_called()
+
+    def test_get_maximum_power_level_in_dbm(self):
+        """Test getting the maximum power_level in dbm."""
+        # Arrange
+        expected_pow = round(self.power_max, 5)  # We use 5 decimals for rounding output pow.
+        # Act
+        power_level = self.instr.get_maximum_power_level()
+        # Assert
+        self.assertEqual(expected_pow, power_level)
+        self._scpi_mock.assert_not_called()
+
+    def test_get_power_level_in_mw(self):
+        """Test getting the power_level in mW."""
+        pow = 2.0
+        expected_pow = round(pow, 2)  # We use 2 decimals for rounding output pow.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{pow}"
+        ]
+        expected_ask_calls = [
+            call().ask(":POW?"),
+        ]
+        # Act
+        power_level = self.instr.get_power_level()
+        # Assert
+        self.assertEqual(expected_pow, power_level)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_get_minimum_power_level_in_mw(self):
+        """Test getting the minimum power_level in mW."""
+        expected_pow = round(self.power_min, 2)  # We use 2 decimals for rounding output pow.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{self.power_min}"
+        ]
+        # Act
+        power_level = self.instr.get_minimum_power_level()
+        # Assert
+        self.assertEqual(expected_pow, power_level)
+        self._scpi_mock.assert_called_once()
+
+    def test_get_maximum_power_level_in_mw(self):
+        """Test getting the maximum power_level in mW."""
+        expected_pow = round(self.power_max, 5)  # We use 5 decimals for rounding output pow.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{self.power_max}"
+        ]
+        # Act
+        power_level = self.instr.get_maximum_power_level()
+        # Assert
+        self.assertEqual(expected_pow, power_level)
+        self._scpi_mock.assert_called_once()
+
+    def test_set_power_level_unit(self):
+        """Test setting the power_level unit."""
+        expected_units = [1, 0]  # is ["mW", "dbm"]
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts.",
+            '0,"No error"',
+            "No alerts.",
+        ]
+        expected_calls = [
+            call().write(f":POW:UNIT {expected_units[0]}"),
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?"),
+            call().write(f":POW:UNIT {expected_units[1]}"),
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?"),
+        ]
+        # Act
+        self.instr.set_power_level_unit_to_mw()
+        self.instr.set_power_level_unit_to_dbm()
+
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_calls)
+
+    def test_get_power_level_unit_dbm(self):
+        """Test getting the power_level unit as dbm."""
+        expected_unit = "dBm"
+        self._scpi_mock(self._transport_mock).ask.side_effect = ["0"]
+        expected_ask_calls = [
+            call().ask(":POW:UNIT?"),
+        ]
+        # Act
+        unit = self.instr.get_power_level_unit()
+        # Assert
+        self.assertEqual(expected_unit, unit)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_get_power_level_unit_mw(self):
+        """Test getting the power_level unit as mW."""
+        expected_unit = "mW"
+        self._scpi_mock(self._transport_mock).ask.side_effect = ["1"]
+        expected_ask_calls = [
+            call().ask(":POW:UNIT?"),
+        ]
+        # Act
+        unit = self.instr.get_power_level_unit()
+        # Assert
+        self.assertEqual(expected_unit, unit)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_start_wavelength_in_nm(self):
+        """Test setting the sweep_start_wavelength in nm."""
+        input_wl = 1450
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV:SWE:STAR {input_wl}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_sweep_start_wavelength(input_wl)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_start_wavelength_in_nm_excepts(self):
+        """Test setting the sweep_start_wavelength in nm with values out-of-bounds."""
+        input_wl = [self.wl_min - 1, self.wl_max + 1]
+        # Act
+        for inp in input_wl:
+            with self.assertRaises(ValueError):
+                self.instr.set_sweep_start_wavelength(inp)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_sweep_start_wavelength_in_nm(self):
+        """Test getting the sweep_start_wavelength in nm."""
+        wl = 1450.0
+        expected_wl = round(wl, 4)  # We use 4 decimals for rounding output wl.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{wl}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:SWE:STAR?"),
+        ]
+        # Act
+        wavelength = self.instr.get_sweep_start_wavelength()
+        # Assert
+        self.assertEqual(expected_wl, wavelength)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_stop_wavelength_in_nm(self):
+        """Test setting the sweep_stop_wavelength in nm."""
+        input_wl = 1450
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV:SWE:STOP {input_wl}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_sweep_stop_wavelength(input_wl)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_stop_wavelength_in_nm_excepts(self):
+        """Test setting the sweep_stop_wavelength in nm with values out-of-bounds."""
+        input_wl = [self.wl_min - 1, self.wl_max + 1]
+        # Act
+        for inp in input_wl:
+            with self.assertRaises(ValueError):
+                self.instr.set_sweep_stop_wavelength(inp)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_sweep_stop_wavelength_in_nm(self):
+        """Test getting the sweep_stop_wavelength in nm."""
+        wl = 1450.0
+        expected_wl = round(wl, 4)  # We use 4 decimals for rounding output wl.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{wl}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:SWE:STOP?"),
+        ]
+        # Act
+        wavelength = self.instr.get_sweep_stop_wavelength()
+        # Assert
+        self.assertEqual(expected_wl, wavelength)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_start_frequency_in_thz(self):
+        """Test setting the sweep_start_frequency in thz."""
+        input_freq = 201
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV:FREQ:SWE:STAR {input_freq}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_sweep_start_frequency(input_freq)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_start_frequency_in_thz_excepts(self):
+        """Test setting the sweep_start_frequency in thz with values out-of-bounds."""
+        input_freq = [self.freq_min - 1, self.freq_max + 1]
+        # Act
+        for inp in input_freq:
+            with self.assertRaises(ValueError):
+                self.instr.set_sweep_start_frequency(inp)
+
+        # Assert
+
+        self._scpi_mock.assert_not_called()
+
+    def test_get_sweep_start_frequency_in_thz(self):
+        """Test getting the sweep_start_frequency in thz."""
+        freq = 1450.0
+        expected_freq = round(freq, 5)  # We use 5 decimals for rounding output freq.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{freq}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:FREQ:SWE:STAR?"),
+        ]
+        # Act
+        frequency = self.instr.get_sweep_start_frequency()
+        # Assert
+        self.assertEqual(expected_freq, frequency)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_stop_frequency_in_thz(self):
+        """Test setting the sweep_stop_frequency in thz."""
+        input_freq = 201
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV:FREQ:SWE:STOP {input_freq}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_sweep_stop_frequency(input_freq)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_stop_frequency_in_thz_excepts(self):
+        """Test setting the sweep_stop_frequency in thz with values out-of-bounds."""
+        input_freq = [self.freq_min - 1, self.freq_max + 1]
+        # Act
+        for inp in input_freq:
+            with self.assertRaises(ValueError):
+                self.instr.set_sweep_stop_frequency(inp)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_sweep_stop_frequency_in_thz(self):
+        """Test getting the sweep_stop_frequency in thz."""
+        freq = 1450.0
+        expected_freq = round(freq, 5)  # We use 5 decimals for rounding output freq.
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{freq}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:FREQ:SWE:STOP?"),
+        ]
+        # Act
+        frequency = self.instr.get_sweep_stop_frequency()
+        # Assert
+        self.assertEqual(expected_freq, frequency)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_mode(self):
+        """Test setting the sweep_mode."""
+        mode = random.randint(0, 3)
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV:SWE:MOD {mode}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_sweep_mode(mode)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_mode_excepts(self):
+        """Test setting the sweep_mode in with values out-of-bounds."""
+        input_mode = [-1, 4]
+        # Act
+        for mode in input_mode:
+            with self.assertRaises(ValueError):
+                self.instr.set_sweep_mode(mode)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_sweep_mode(self):
+        """Test getting the sweep_mode."""
+        expected_mode = 2
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{expected_mode}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:SWE:MOD?"),
+        ]
+        # Act
+        mode = self.instr.get_sweep_mode()
+        # Assert
+        self.assertEqual(expected_mode, mode)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_speed(self):
+        """Test setting the sweep_speed."""
+        speed = random.choice([1, 2, 5, 10, 20, 50, 100, 200])
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV:SWE:SPE {speed}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_sweep_speed(speed)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_speed_excepts(self):
+        """Test setting the sweep_speed in with values out-of-bounds."""
+        input_speed = [0, 199]
+        # Act
+        for speed in input_speed:
+            with self.assertRaises(ValueError):
+                self.instr.set_sweep_speed(speed)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_sweep_speed(self):
+        """Test getting the sweep_speed in nm/s."""
+        expected_speed = 2
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{expected_speed}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:SWE:SPE?"),
+        ]
+        # Act
+        speed = self.instr.get_sweep_speed()
+        # Assert
+        self.assertEqual(expected_speed, speed)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_cycles(self):
+        """Test setting the sweep_cycles."""
+        cycles = random.randint(0, 999)
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV:SWE:CYCL {cycles}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_sweep_cycles(cycles)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_cycles_excepts(self):
+        """Test setting the sweep_cycles in with values out-of-bounds."""
+        input_cycles = [-1, 1000]
+        # Act
+        for cycles in input_cycles:
+            with self.assertRaises(ValueError):
+                self.instr.set_sweep_cycles(cycles)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_sweep_cycles(self):
+        """Test getting the sweep_cycles."""
+        expected_cycles = 2
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{expected_cycles}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:SWE:CYCL?"),
+        ]
+        # Act
+        cycles = self.instr.get_sweep_cycles()
+        # Assert
+        self.assertEqual(expected_cycles, cycles)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_dwell(self):
+        """Test setting the sweep_dwell."""
+        dwell = float(random.randint(0, 999))
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV:SWE:DWEL {dwell}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_sweep_dwell(dwell)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_dwell_excepts(self):
+        """Test setting the sweep_dwell in with values out-of-bounds."""
+        input_dwell = [-0.1, 1000.0]
+        # Act
+        for dwell in input_dwell:
+            with self.assertRaises(ValueError):
+                self.instr.set_sweep_dwell(dwell)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_sweep_dwell(self):
+        """Test getting the sweep_dwell."""
+        expected_dec = 1  # should be 1 decimal accuracy
+        expected_dwell = 2.0
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{expected_dwell:.{expected_dec}f}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:SWE:DWEL?"),
+        ]
+        # Act
+        dwell = self.instr.get_sweep_dwell()
+        # Assert
+        self.assertEqual(expected_dwell, dwell)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_delay(self):
+        """Test setting the sweep_delay."""
+        delay = float(random.randint(0, 999))
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV:SWE:DEL {delay}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_sweep_delay(delay)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_delay_excepts(self):
+        """Test setting the sweep_delay in with values out-of-bounds."""
+        input_delay = [-0.1, 1000.0]
+        # Act
+        for delay in input_delay:
+            with self.assertRaises(ValueError):
+                self.instr.set_sweep_delay(delay)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_sweep_delay(self):
+        """Test getting the sweep_delay."""
+        expected_dec = 1  # should be 1 decimal accuracy
+        expected_delay = 2
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{expected_delay:.{expected_dec}f}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:SWE:DEL?"),
+        ]
+        # Act
+        delay = self.instr.get_sweep_delay()
+        # Assert
+        self.assertEqual(expected_delay, delay)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_state(self):
+        """Test setting the sweep_state."""
+        state = random.randint(0, 1)
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":WAV:SWE:STAT {state}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_sweep_state(state)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_sweep_state_excepts(self):
+        """Test setting the sweep_state in with values out-of-bounds."""
+        input_state = [-1, 2]
+        # Act
+        for state in input_state:
+            with self.assertRaises(ValueError):
+                self.instr.set_sweep_state(state)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_sweep_state(self):
+        """Test getting the sweep_state."""
+        expected_state = 2
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{expected_state}"
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:SWE:STAT?"),
+        ]
+        # Act
+        state = self.instr.get_sweep_state()
+        # Assert
+        self.assertEqual(expected_state, state)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_trigger_output_timing_mode(self):
+        """Test setting the trigger_output_timing_mode."""
+        mode = random.randint(0, 3)
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":TRIG:OUTP {mode}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_trigger_output_timing_mode(mode)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_trigger_output_timing_mode_excepts(self):
+        """Test setting the trigger_output_timing_mode in with values out-of-bounds."""
+        input_mode = [-1, 4]
+        # Act
+        for mode in input_mode:
+            with self.assertRaises(ValueError):
+                self.instr.set_trigger_output_timing_mode(mode)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_trigger_output_timing_mode(self):
+        """Test getting the trigger_output_timing_mode."""
+        expected_mode = 2
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{expected_mode}"
+        ]
+        expected_ask_calls = [
+            call().ask(":TRIG:OUTP?"),
+        ]
+        # Act
+        mode = self.instr.get_trigger_output_timing_mode()
+        # Assert
+        self.assertEqual(expected_mode, mode)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_trigger_output_period_mode(self):
+        """Test setting the trigger_output_period_mode."""
+        mode = random.randint(0, 1)
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":TRIG:OUTP:SETT {mode}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_trigger_output_period_mode(mode)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_trigger_output_period_mode_excepts(self):
+        """Test setting the trigger_output_period_mode in with values out-of-bounds."""
+        input_mode = [-1, 2]
+        # Act
+        for mode in input_mode:
+            with self.assertRaises(ValueError):
+                self.instr.set_trigger_output_period_mode(mode)
+
+        # Assert
+        self._scpi_mock.assert_not_called()
+
+    def test_get_trigger_output_period_mode(self):
+        """Test getting the trigger_output_period_mode."""
+        expected_mode = 2
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{expected_mode}"
+        ]
+        expected_ask_calls = [
+            call().ask(":TRIG:OUTP:SETT?"),
+        ]
+        # Act
+        mode = self.instr.get_trigger_output_period_mode()
+        # Assert
+        self.assertEqual(expected_mode, mode)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_trigger_output_step(self):
+        """Test setting the trigger_output_step."""
+        dec = 4  # Response in 4 decimal accuracy
+        sweep_stop_wl, sweep_start_wl = 1360.0, 1280.0
+        wl_range = sweep_stop_wl - sweep_start_wl
+        step = random.uniform(0.0001, wl_range)
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{sweep_stop_wl}",
+            f"{sweep_start_wl}",
+            '0,"No error"',
+            "No alerts."
+        ]
+        expected_write_calls = [
+            call().write(f":TRIG:OUTP:STEP {step:.{dec}f}")
+        ]
+        expected_ask_calls = [
+            call().ask(":SYST:ERR?"),
+            call().ask(":SYST:ALER?")
+        ]
+        # Act
+        self.instr.set_trigger_output_step(step)
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_set_trigger_output_step_excepts(self):
+        """Test setting the trigger_output_step in with values out-of-bounds."""
+        sweep_stop_wl, sweep_start_wl = 1360.0, 1280.0
+        wl_range = sweep_stop_wl - sweep_start_wl
+        input_step = [0.0000, wl_range + 0.0001]
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{sweep_stop_wl}",
+            f"{sweep_start_wl}",
+            f"{sweep_stop_wl}",
+            f"{sweep_start_wl}",
+        ]
+        expected_ask_calls = [
+            call().ask(":WAV:SWE:STOP?"),
+            call().ask(":WAV:SWE:STAR?"),
+            call().ask(":WAV:SWE:STOP?"),
+            call().ask(":WAV:SWE:STAR?")
+        ]
+        # Act
+        for step in input_step:
+            with self.assertRaises(ValueError):
+                self.instr.set_trigger_output_step(step)
+
+        # Assert
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_get_trigger_output_step(self):
+        """Test getting the trigger_output_step."""
+        expected_dec = 1  # should be 1 decimal accuracy
+        expected_step = 2
+        self._scpi_mock(self._transport_mock).ask.side_effect = [
+            f"{expected_step:.{expected_dec}f}"
+        ]
+        expected_ask_calls = [
+            call().ask(":TRIG:OUTP:STEP?"),
+        ]
+        # Act
+        step = self.instr.get_trigger_output_step()
+        # Assert
+        self.assertEqual(expected_step, step)
+        self._scpi_mock.assert_has_calls(expected_ask_calls)
+
+    def test_issue_soft_trigger(self):
+        """Test issue_soft_trigger."""
+        self._scpi_mock(self._transport_mock).ask.side_effect = ['0,"No error"', "No alerts."]
+
+        self.instr.issue_soft_trigger()
+
+        self._scpi_mock.assert_has_calls([call().write(":TRIG:INP:SOFT")])
+        self._scpi_mock.assert_has_calls([call().ask(":SYST:ERR?")])
+        self._scpi_mock.assert_has_calls([call().ask(":SYST:ALER?")])
+
+    def test_readout_points(self):
+        """Test getting number of readout points."""
+        readout_points = 124
+        self._scpi_mock(self._transport_mock).ask.side_effect = [f"{readout_points}"]
+
+        points = self.instr.readout_points()
+
+        self.assertEqual(readout_points, points)
+        self._scpi_mock.assert_has_calls([call().ask(":READ:POIN?")])
+
+    def test_readout_data(self):
+        """Test reading out data."""
+        # Let's make 200 data point readings
+        expected_data_points = 200
+        expected_data = [round(random.uniform(self.wl_min, self.wl_max), 4) for _ in range(expected_data_points)]
+        binary_data = b''.join([int(d * 1e4).to_bytes(4, byteorder="little") for d in expected_data])
+        self._scpi_mock(self._transport_mock).read_binary_data.side_effect = [
+            binary_data
+        ]
+        expected_write_calls = [
+            call().write(":READout:DATa?"),
+        ]
+        # Act
+        data = self.instr.readout_data()
+        # Assert
+        # Use numpy list assertion as otherwise some float inaccuracy errors appear
+        np.testing.assert_allclose(expected_data, data, rtol=1e-4)
+        self._scpi_mock.assert_has_calls(expected_write_calls)
+
+    def test_shutdown(self):
+        """Test shutdown."""
+        self._scpi_mock(self._transport_mock).ask.side_effect = ['0,"No error"', "No alerts."]
+
+        self.instr.shutdown()
+
+        self._scpi_mock.assert_has_calls([call().write(":SPEC:SHUT")])
+        self._scpi_mock.assert_has_calls([call().ask(":SYST:ERR?")])
+        self._scpi_mock.assert_has_calls([call().ask(":SYST:ALER?")])
+
+    def test_reboot(self):
+        """Test reboot."""
+        self._scpi_mock(self._transport_mock).ask.side_effect = ['0,"No error"', "No alerts."]
+
+        self.instr.reboot()
+
+        self._scpi_mock.assert_has_calls([call().write(":SPECial:REBoot")])
+        self._scpi_mock.assert_has_calls([call().ask(":SYST:ERR?")])
+        self._scpi_mock.assert_has_calls([call().ask(":SYST:ALER?")])