Commit e8c91f0e authored by vertighel's avatar vertighel
Browse files

Google AI studio Revised versions of astelco and alpaca devices

parent 5a9387aa
Loading
Loading
Loading
Loading
Loading
+591 −0
Original line number Diff line number Diff line
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Interface with an ASCOM Alpaca device.
"""

# System modules
import time
from urllib.parse import urlencode

# Third-party modules
import requests

# Other templates
from ..config.constants import dome_park_position
from ..utils import check
from ..utils.logger import log
from .basedevice import BaseDevice


class AlpacaDevice(BaseDevice):
    """Base wrapper class for ASCOM Alpaca devices."""

    def __init__(self, url, dev=None, device_number=0):
        """
        Initializes the Alpaca device.

        Parameters
        ----------
        url : str
            The base URL of the Alpaca server (e.g., "http://192.168.1.100:11111").
        dev : str, optional
            The device type (e.g., "telescope", "dome").
        device_number : int, optional
            The device number on the Alpaca server, by default 0.
        """
        super().__init__(url)
        self.url = url
        self._dev = dev
        self.device_number = device_number
        self.timeout = 10

    @property
    def addr(self):
        """
        str: The full API endpoint address for the device.
        """
        return f"{self.url}/api/v1/{self._dev}/{self.device_number}"

    @check.request_errors
    def get(self, method, params=None, timeout=None):
        """
        Sends a HTTP GET request to the device.

        Parameters
        ----------
        method : str
            The Alpaca API method name (e.g., "temperature").
        params : dict, optional
            A dictionary of query parameters.
        timeout : int, optional
            Request timeout in seconds. Defaults to the class timeout.

        Returns
        -------
        any or None
            The "Value" field from the Alpaca JSON response, or None on error.
        """
        if params is None:
            params = {}
        if timeout is None:
            timeout = self.timeout

        res = requests.get(f"{self.addr}/{method}", params=params, timeout=timeout)
        res.raise_for_status()

        resj = res.json()
        if resj["ErrorNumber"] != 0:
            msg = f'ASCOM {resj["ErrorNumber"]}: {resj["ErrorMessage"]}'
            log.error(msg)
            self.error.append(msg)
            return None
        
        return resj.get("Value", None)

    @check.request_errors
    def put(self, method, data=None, timeout=None):
        """
        Sends a HTTP PUT request to the device.

        Parameters
        ----------
        method : str
            The Alpaca API method name (e.g., "connected").
        data : dict, optional
            A dictionary of form data to send.
        timeout : int, optional
            Request timeout in seconds. Defaults to the class timeout.

        Returns
        -------
        any or None
            The "Value" field if present, True on success, or None on error.
        """
        if data is None:
            data = {}
        if timeout is None:
            timeout = self.timeout

        res = requests.put(f"{self.addr}/{method}", data=data, timeout=timeout)
        res.raise_for_status()

        resj = res.json()
        if resj["ErrorNumber"] != 0:
            msg = f'ASCOM {resj["ErrorNumber"]}: {resj["ErrorMessage"]}'
            log.error(msg)
            self.error.append(msg)
            return None
        
        return resj.get("Value", True)

    @property
    def name(self):
        """str or None: The name of the device."""
        
        res = self.get("name")
        if self.error:
            return None
        return res

    @property
    def connection(self):
        """bool or None: The connection status of the device."""

        res = self.get("connected")
        if self.error:
            return None
        return res

    @connection.setter
    def connection(self, b):
        """
        Sets the connection status of the device.

        Parameters
        ----------
        b : bool
            True to connect, False to disconnect.
        """

        data = {"Connected": str(b)}
        self.put("connected", data=data)

    def command(self, cmd_string, raw=False, action="commandstring"):
        """
        Sends a command string to the device via a non-standard action.

        Note
        ----
        This method uses a custom 'commandstring' or 'commandblind' action,
        which is a non-standard Alpaca extension. Its use indicates that this
        driver is tailored to a specific server implementation that tunnels
        other protocols (like OpenTSI) through Alpaca.

        Parameters
        ----------
        cmd_string : str
            The command string to execute.
        raw : bool, optional
            Indicates if the command is a raw command, by default False.
        action : str, optional
            The Alpaca action to use, either "commandstring" or "commandblind".

        Returns
        -------
        any or None
            The result of the command, or None on error.
        """
        data = {"Command": cmd_string, "Raw": str(raw)}
        res = self.put(action, data=data)
        if self.error:
            return None
        return res

    def commandblind(self, cmd_string, raw=False):
        """
        Sends a "fire and forget" command string to the device.

        See Also
        --------
        command
        """
        return self.command(cmd_string, raw, action="commandblind")


class Switch(AlpacaDevice):
    """Wrapper for ASCOM Alpaca devices of "switch" type."""

    def __init__(self, url, switch_id, device_number=0):
        """
        Initializes the Switch interface.

        Parameters
        ----------
        url : str
            The base URL of the Alpaca server.
        switch_id : int
            The ID of the specific switch device to control.
        device_number : int, optional
            The device number of the switch controller on the server, by default 0.
        """
        super().__init__(url, dev="switch", device_number=device_number)
        self.switch_id = switch_id

    @property
    def description(self):
        """str or None: The name or description of the switch."""

        params = {"Id": self.switch_id}
        res = self.get("getswitchname", params=params)
        if self.error:
            return None
        return res

    @property
    def state(self):
        """
        bool or None: The current state of the switch.
        True if on, False if off.
        """

        params = {"Id": self.switch_id}
        res = self.get("getswitch", params=params)
        if self.error:
            return None
        return res

    @state.setter
    def state(self, b):
        """
        Sets the state of the switch.

        Parameters
        ----------
        b : bool
            True to turn the switch on, False to turn it off.
        """

        data = {"Id": self.switch_id, "State": str(b)}
        self.put("setswitch", data=data)

        
class Dome(AlpacaDevice):
    """Wrapper for ASCOM Alpaca devices of "dome" type."""

    def __init__(self, url, device_number=0):
        """
        Initializes the Dome interface.
        """
        super().__init__(url, dev="dome", device_number=device_number)
        self.PARK = dome_park_position
        self.TOLERANCE = 1.0  # deg tolerance for park

    def park(self):
        """Puts the dome in its park position."""

        self.put("park")

    def sync(self, az):
        """
        Synchronizes the dome's azimuth to a specific value.
        """

        data = {"Azimuth": az}
        self.put("synctoazimuth", data=data)

    def abort(self):
        """Aborts any dome motion."""

        self.put("abortslew")

    @property
    def is_parked(self):
        """bool or None: Checks if the dome is at its park position."""

        res = self.get("atpark")
        if self.error:
            return None
        return res

    @property
    def is_moving(self):
        """bool or None: Checks if the dome is slewing."""

        res = self.get("slewing")
        if self.error:
            return None
        return res

    @property
    def azimuth(self):
        """float or None: The current dome azimuth in degrees."""

        res = self.get("azimuth")
        if self.error:
            return None
        return res

    @azimuth.setter
    def azimuth(self, a):
        """
        Slews the dome to a new azimuth.

        Parameters
        ----------
        a : float
            The target azimuth in degrees.
        """

        data = {"Azimuth": a}
        self.put("slewtoazimuth", data=data)

    @property
    def shutter(self):
        """
        int or None: The status of the dome shutter.

        (0=Open, 1=Closed, 2=Opening, 3=Closing, 4=Error)
        """

        res = self.get("shutterstatus")
        if self.error:
            return None
        return res

    @property
    def open(self):
        """
        bool or None: The dome shutter position.
        True if open, False if closed, None otherwise.
        """

        res = self.shutter
        if res == 0:
            return True
        elif res == 1:
            return False
        return None

    @open.setter
    def open(self, b):
        """
        Opens or closes the dome shutter.
        """

        if b:
            self.put("openshutter")
        else:
            self.put("closeshutter")

    @property
    def slave(self):
        """
        bool or None: The slaved state of the dome.
        """

        res = self.get("slaved")
        if self.error:
            return None
        return res

    @slave.setter
    def slave(self, b):
        """
        Slaves or un-slaves the dome from the telescope.
        """

        data = {"Slaved": str(b)}
        self.put("slaved", data=data)


class Telescope(AlpacaDevice):
    """Wrapper for ASCOM Alpaca devices of "telescope" type."""

    def __init__(self, url, device_number=0):
        """
        Initializes the Telescope interface.
        """
        super().__init__(url, dev="telescope", device_number=device_number)

    def track(self):
        """
        Slews to the current target coordinates and begins tracking.
        """

        self.tracking = True
        self.put("slewtotargetasync")

    def abort(self):
        """Aborts any telescope motion."""

        self.put("abortslew")

    @property
    def is_moving(self):
        """bool or None: Checks if the telescope is slewing."""

        res = self.get("slewing")
        if self.error:
            return None
        return res

    @property
    def state(self):
        """
        int or None: The global operational status of the telescope.
        Uses a non-standard command.
        """

        res = self.command("TELESCOPE.STATUS.GLOBAL")
        if self.error:
            return None
        return res

    @property
    def park(self):
        """bool or None: Checks if the telescope is parked."""

        res = self.get("atpark")
        if self.error:
            return None
        return res

    @park.setter
    def park(self, b, timeout=120):
        """
        Parks or unparks the telescope.
        """

        if b:
            self.put("park", timeout=timeout)
        else:
            self.put("unpark", timeout=timeout)

    @property
    def altaz(self):
        """list or None: Current [altitude, azimuth] in degrees."""

        alt = self.get("altitude")
        if self.error:
            return [None, None]
        az = self.get("azimuth")
        if self.error:
            return [None, None]
        return [alt, az]

    @altaz.setter
    def altaz(self, a):
        """
        Slews the telescope to the given Alt/Az coordinates.
        """

        self.tracking = False
        data = {"Altitude": a[0], "Azimuth": a[1]}
        self.put("slewtoaltazasync", data=data)

    @property
    def radec(self):
        """list or None: Current [RA, Dec] in [hours, degrees]."""

        ra = self.get("rightascension")
        if self.error:
            return [None, None]
        dec = self.get("declination")
        if self.error:
            return [None, None]
        return [ra, dec]

    @radec.setter
    def radec(self, a):
        """
        Slews to the given RA/Dec coordinates and tracks.
        """

        self.tracking = True
        data = {"RightAscension": a[0], "Declination": a[1]}
        self.put("slewtocoordinatesasync", data=data)

    @property
    def tracking(self):
        """bool or None: The sidereal tracking state of the telescope."""

        res = self.get("tracking")
        if self.error:
            return None
        return res

    @tracking.setter
    def tracking(self, b):
        """
        Enables or disables sidereal tracking.
        """

        data = {"Tracking": str(b)}
        self.put("tracking", data=data)


class Focuser(AlpacaDevice):
    """Wrapper for ASCOM Alpaca devices of "focuser" type."""

    def __init__(self, url, device_number=0):
        """
        Initializes the Focuser interface.
        """
        super().__init__(url, dev="focuser", device_number=device_number)

    @property
    def is_moving(self):
        """bool or None: Checks if the focuser is currently moving."""

        res = self.get("ismoving")
        if self.error:
            return None
        return res

    @property
    def position(self):
        """int or None: The current focuser position in steps or microns."""

        res = self.get("position")
        if self.error:
            return None
        return res

    @position.setter
    def position(self, s):
        """
        Moves the focuser to a new absolute position.

        Parameters
        ----------
        s : int
            The target position in steps or microns, as defined by the device.
        """

        data = {"Position": s}
        self.put("move", data=data)


class Rotator(AlpacaDevice):
    """Wrapper for ASCOM Alpaca devices of "rotator" type."""

    def __init__(self, url, device_number=0):
        """
        Initializes the Rotator interface.
        """
        super().__init__(url, dev="rotator", device_number=device_number)

    @property
    def is_moving(self):
        """bool or None: Checks if the rotator is currently moving."""

        res = self.get("ismoving")
        if self.error:
            return None
        return res

    @property
    
    def position(self):
        """float or None: The current rotator position in degrees."""

        res = self.get("position")
        if self.error:
            return None
        return res

    @position.setter
    def position(self, s):
        """
        Moves the rotator to a new absolute position.

        Parameters
        ----------
        s : float
            The target position in degrees.
        """
        
        data = {"Position": s}
        self.put("moveabsolute", data=data)
+714 −0

File added.

Preview size limit exceeded, changes collapsed.