pyobs-alpaca 2.0.0.dev2__tar.gz → 2.0.0.dev4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyobs_alpaca-2.0.0.dev4/.flake8 +14 -0
- pyobs_alpaca-2.0.0.dev4/.github/dependabot.yml +15 -0
- pyobs_alpaca-2.0.0.dev4/.github/workflows/pyrefly.yml +23 -0
- pyobs_alpaca-2.0.0.dev4/.github/workflows/pytest.yml +25 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/.readthedocs.yml +1 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/PKG-INFO +3 -3
- pyobs_alpaca-2.0.0.dev4/README.md +65 -0
- pyobs_alpaca-2.0.0.dev4/docs/requirements.txt +1 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/pyobs_alpaca/focuser.py +4 -4
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/pyobs_alpaca/telescope.py +8 -3
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/pyproject.toml +8 -3
- pyobs_alpaca-2.0.0.dev4/tests/test_device.py +68 -0
- pyobs_alpaca-2.0.0.dev4/tests/test_import.py +47 -0
- pyobs_alpaca-2.0.0.dev4/uv.lock +2604 -0
- pyobs_alpaca-2.0.0.dev2/README.md +0 -6
- pyobs_alpaca-2.0.0.dev2/uv.lock +0 -2107
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/.github/workflows/pypi.yml +0 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/.github/workflows/ruff.yml +0 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/.pre-commit-config.yaml +0 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/CHANGELOG.rst +0 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/LICENSE +0 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/docs/Makefile +0 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/docs/make.bat +0 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/docs/source/_static/pyobs.gif +0 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/docs/source/conf.py +0 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/docs/source/index.rst +0 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/pyobs_alpaca/__init__.py +0 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/pyobs_alpaca/device.py +0 -0
- {pyobs_alpaca-2.0.0.dev2 → pyobs_alpaca-2.0.0.dev4}/pyobs_alpaca/dome.py +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[flake8]
|
|
2
|
+
ignore =
|
|
3
|
+
E203, E266, E501, W503, F403,
|
|
4
|
+
# E704: Multiple statements on one line (def)
|
|
5
|
+
E704,
|
|
6
|
+
# E722: do not use bare 'except'
|
|
7
|
+
E722
|
|
8
|
+
# E701: multiple statements on one line (colon)
|
|
9
|
+
E701
|
|
10
|
+
per-file-ignores = __init__.py:F401
|
|
11
|
+
max-line-length = 120
|
|
12
|
+
max-complexity = 30
|
|
13
|
+
select = B, C, E, F, W, T4, B9
|
|
14
|
+
exclude = .git,.venv,__pycache__,docs,build,dist
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: pyrefly
|
|
2
|
+
on: push
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
pyrefly:
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
timeout-minutes: 10
|
|
8
|
+
|
|
9
|
+
steps:
|
|
10
|
+
- name: Check out repository code
|
|
11
|
+
uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- name: Set up Python
|
|
14
|
+
uses: astral-sh/setup-uv@v5
|
|
15
|
+
with:
|
|
16
|
+
enable-cache: true
|
|
17
|
+
python-version: "3.13"
|
|
18
|
+
|
|
19
|
+
- name: Install packages
|
|
20
|
+
run: uv sync --all-extras --dev
|
|
21
|
+
|
|
22
|
+
- name: Run pyrefly
|
|
23
|
+
run: uv run pyrefly check
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: PyTest
|
|
2
|
+
on: push
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
test:
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
timeout-minutes: 10
|
|
8
|
+
|
|
9
|
+
steps:
|
|
10
|
+
- name: Check out repository code
|
|
11
|
+
uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- name: Set up Python
|
|
14
|
+
uses: astral-sh/setup-uv@v5
|
|
15
|
+
with:
|
|
16
|
+
enable-cache: true
|
|
17
|
+
python-version: "3.13"
|
|
18
|
+
|
|
19
|
+
- name: Install packages
|
|
20
|
+
run: uv sync --all-extras --dev
|
|
21
|
+
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: uv run pytest tests
|
|
24
|
+
env:
|
|
25
|
+
CI: true
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: pyobs-alpaca
|
|
3
|
-
Version: 2.0.0.
|
|
3
|
+
Version: 2.0.0.dev4
|
|
4
4
|
Summary: pyobs module for ASCOM Alpaca
|
|
5
5
|
Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Requires-Dist: numpy<3,>=2.2.5
|
|
10
|
-
Requires-Dist: pyobs-core<3,>=2.0.0.
|
|
10
|
+
Requires-Dist: pyobs-core<3,>=2.0.0.dev48
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
ASCOM Alpaca wrapper for *pyobs*
|
|
2
|
+
================================
|
|
3
|
+
|
|
4
|
+
This is a [pyobs](https://www.pyobs.org) module for wrapping devices accessible via
|
|
5
|
+
[ASCOM Alpaca](https://ascom-standards.org/AlpacaDeveloper/), a HTTP proxy for ASCOM. It provides
|
|
6
|
+
telescope, focuser and dome implementations.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Install *pyobs-alpaca*
|
|
10
|
+
-----------------------
|
|
11
|
+
Clone the repository:
|
|
12
|
+
|
|
13
|
+
git clone https://github.com/pyobs/pyobs-alpaca.git
|
|
14
|
+
cd pyobs-alpaca
|
|
15
|
+
|
|
16
|
+
Install it with [uv](https://docs.astral.sh/uv/):
|
|
17
|
+
|
|
18
|
+
uv sync
|
|
19
|
+
|
|
20
|
+
Alternatively, with plain `venv`/`pip`:
|
|
21
|
+
|
|
22
|
+
python3 -m venv .venv
|
|
23
|
+
source .venv/bin/activate
|
|
24
|
+
pip install .
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Configuration
|
|
28
|
+
-------------
|
|
29
|
+
This is an example configuration for a telescope:
|
|
30
|
+
|
|
31
|
+
class: pyobs_alpaca.AlpacaTelescope
|
|
32
|
+
name: ASCOM Telescope
|
|
33
|
+
server: 1.2.3.4
|
|
34
|
+
port: 11111
|
|
35
|
+
device_type: telescope
|
|
36
|
+
device: 0
|
|
37
|
+
wait_for_dome: dome
|
|
38
|
+
weather: weather
|
|
39
|
+
alive_parameter: Name
|
|
40
|
+
|
|
41
|
+
And for a focusing unit:
|
|
42
|
+
|
|
43
|
+
class: pyobs_alpaca.AlpacaFocuser
|
|
44
|
+
name: ASCOM Focuser
|
|
45
|
+
server: 1.2.3.4
|
|
46
|
+
port: 11111
|
|
47
|
+
device_type: focuser
|
|
48
|
+
device: 0
|
|
49
|
+
alive_parameter: Position
|
|
50
|
+
|
|
51
|
+
And finally, for a dome:
|
|
52
|
+
|
|
53
|
+
class: pyobs_alpaca.AlpacaDome
|
|
54
|
+
server: 1.2.3.4
|
|
55
|
+
port: 11111
|
|
56
|
+
device_type: dome
|
|
57
|
+
device: 0
|
|
58
|
+
follow: telescope
|
|
59
|
+
weather: weather
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
Dependencies
|
|
63
|
+
------------
|
|
64
|
+
* [pyobs-core](https://github.com/pyobs/pyobs-core) for the core functionality.
|
|
65
|
+
* [NumPy](https://numpy.org/) for array handling.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sphinx-rtd-theme
|
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
import logging
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from pyobs.interfaces import FocuserState, IFitsHeaderBefore, IFocuser, IReady, ReadyState
|
|
5
|
+
from pyobs.interfaces import FitsHeaderEntry, FocuserState, IFitsHeaderBefore, IFocuser, IReady, ReadyState
|
|
6
6
|
from pyobs.mixins import MotionStatusMixin
|
|
7
7
|
from pyobs.modules import Module, timeout
|
|
8
8
|
from pyobs.utils import exceptions as exc
|
|
@@ -34,7 +34,7 @@ class AlpacaFocuser(MotionStatusMixin, IFocuser, IFitsHeaderBefore, Module):
|
|
|
34
34
|
MotionStatusMixin.__init__(self, motion_status_interfaces=["IFocuser"])
|
|
35
35
|
|
|
36
36
|
# register exception
|
|
37
|
-
|
|
37
|
+
self._register_exception(exc.MotionError, 3, timespan=600, callback=self._default_remote_error_callback)
|
|
38
38
|
|
|
39
39
|
async def open(self) -> None:
|
|
40
40
|
"""Open module."""
|
|
@@ -57,13 +57,13 @@ class AlpacaFocuser(MotionStatusMixin, IFocuser, IFitsHeaderBefore, Module):
|
|
|
57
57
|
|
|
58
58
|
async def get_fits_header_before(
|
|
59
59
|
self, namespaces: list[str] | None = None, **kwargs: Any
|
|
60
|
-
) -> dict[str,
|
|
60
|
+
) -> dict[str, FitsHeaderEntry]:
|
|
61
61
|
"""Returns FITS header for the current status of this module."""
|
|
62
62
|
|
|
63
63
|
try:
|
|
64
64
|
pos = await self._device.get("Position")
|
|
65
65
|
step = await self._device.get("StepSize") * 1000.0
|
|
66
|
-
return {"TEL-FOCU": (pos / step, "Focus of telescope [mm]")}
|
|
66
|
+
return {"TEL-FOCU": FitsHeaderEntry(pos / step, "Focus of telescope [mm]")}
|
|
67
67
|
except ConnectionError as e:
|
|
68
68
|
log.warning("Could not determine focus position: %s", e)
|
|
69
69
|
return {}
|
|
@@ -6,6 +6,7 @@ import numpy as np
|
|
|
6
6
|
from pyobs.events import OffsetsRaDecEvent
|
|
7
7
|
from pyobs.interfaces import (
|
|
8
8
|
AltAzState,
|
|
9
|
+
FitsHeaderEntry,
|
|
9
10
|
IFitsHeaderBefore,
|
|
10
11
|
IOffsetsRaDec,
|
|
11
12
|
IPointingAltAz,
|
|
@@ -219,6 +220,10 @@ class AlpacaTelescope(BaseTelescope, FitsNamespaceMixin, IFitsHeaderBefore, IOff
|
|
|
219
220
|
await self.stop_motion()
|
|
220
221
|
raise exc.MoveError("Could not move telescope to RA/Dec.")
|
|
221
222
|
|
|
223
|
+
async def _set_tracking_rate(self, ra_rate: float, dec_rate: float) -> None:
|
|
224
|
+
"""Tracking rates are not supported by the Alpaca telescope driver."""
|
|
225
|
+
raise NotImplementedError
|
|
226
|
+
|
|
222
227
|
@timeout(10000)
|
|
223
228
|
async def set_offsets_radec(self, dra: float, ddec: float, **kwargs: Any) -> None:
|
|
224
229
|
"""Move an RA/Dec offset."""
|
|
@@ -295,14 +300,14 @@ class AlpacaTelescope(BaseTelescope, FitsNamespaceMixin, IFitsHeaderBefore, IOff
|
|
|
295
300
|
|
|
296
301
|
async def get_fits_header_before(
|
|
297
302
|
self, namespaces: list[str] | None = None, **kwargs: Any
|
|
298
|
-
) -> dict[str,
|
|
303
|
+
) -> dict[str, FitsHeaderEntry]:
|
|
299
304
|
"""Returns FITS header for the current status of this module."""
|
|
300
305
|
|
|
301
306
|
hdr = await BaseTelescope.get_fits_header_before(self)
|
|
302
307
|
|
|
303
308
|
try:
|
|
304
|
-
hdr["RAOFF"] = (self._offset_ra, "RA offset [deg]")
|
|
305
|
-
hdr["DECOFF"] = (self._offset_dec, "Dec offset [deg]")
|
|
309
|
+
hdr["RAOFF"] = FitsHeaderEntry(self._offset_ra, "RA offset [deg]")
|
|
310
|
+
hdr["DECOFF"] = FitsHeaderEntry(self._offset_dec, "Dec offset [deg]")
|
|
306
311
|
return self._filter_fits_namespace(hdr, namespaces=namespaces, **kwargs)
|
|
307
312
|
except ConnectionError:
|
|
308
313
|
return {}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyobs-alpaca"
|
|
3
|
-
version = "2.0.0.
|
|
3
|
+
version = "2.0.0.dev4"
|
|
4
4
|
description = "pyobs module for ASCOM Alpaca"
|
|
5
5
|
authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
license = "MIT"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"numpy>=2.2.5,<3",
|
|
10
|
-
"pyobs-core>=2.0.0.
|
|
10
|
+
"pyobs-core>=2.0.0.dev48,<3",
|
|
11
11
|
]
|
|
12
12
|
|
|
13
13
|
[dependency-groups]
|
|
14
14
|
dev = [
|
|
15
|
-
"black>=25.1.0,<
|
|
15
|
+
"black>=25.1.0,<27",
|
|
16
16
|
"ruff>=0.9.0",
|
|
17
17
|
"pyrefly>=1.1.1",
|
|
18
18
|
"pre-commit>=4.2.0,<5",
|
|
19
|
+
"pytest>=8.0.0",
|
|
20
|
+
"pytest-asyncio>=0.24.0",
|
|
19
21
|
]
|
|
20
22
|
|
|
21
23
|
[build-system]
|
|
@@ -35,3 +37,6 @@ select = ["E", "F", "W", "I", "UP", "G"]
|
|
|
35
37
|
|
|
36
38
|
[tool.pyrefly]
|
|
37
39
|
python-version = "3.11"
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
asyncio_mode = "strict"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Unit tests for the pure, non-hardware logic in AlpacaDevice.
|
|
2
|
+
|
|
3
|
+
Hardware I/O (open/close, talking to the ASCOM server) is out of scope here; these cover
|
|
4
|
+
URL construction, version validation, the connection guard, and the connected-state
|
|
5
|
+
transition logic.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from pyobs_alpaca.device import AlpacaDevice
|
|
13
|
+
|
|
14
|
+
_ARGS = dict(server="myserver", port=11111, device_type="telescope", device=3)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_build_alpaca_url() -> None:
|
|
18
|
+
device = AlpacaDevice(**_ARGS)
|
|
19
|
+
assert device._build_alpaca_url("RightAscension") == ("http://myserver:11111/api/v1/telescope/3/rightascension")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_only_v1_supported() -> None:
|
|
23
|
+
with pytest.raises(ValueError):
|
|
24
|
+
AlpacaDevice(**_ARGS, version="v2")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_starts_disconnected() -> None:
|
|
28
|
+
device = AlpacaDevice(**_ARGS)
|
|
29
|
+
assert device.connected is False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.asyncio
|
|
33
|
+
async def test_get_requires_connection() -> None:
|
|
34
|
+
device = AlpacaDevice(**_ARGS)
|
|
35
|
+
with pytest.raises(ConnectionError):
|
|
36
|
+
await device.get("RightAscension")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_put_requires_connection() -> None:
|
|
41
|
+
device = AlpacaDevice(**_ARGS)
|
|
42
|
+
with pytest.raises(ConnectionError):
|
|
43
|
+
await device.put("Tracking", Tracking=False)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.asyncio
|
|
47
|
+
async def test_check_connected_sets_flag_on_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
48
|
+
device = AlpacaDevice(**_ARGS)
|
|
49
|
+
|
|
50
|
+
async def fake_get(name: str) -> Any:
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
monkeypatch.setattr(device, "_get", fake_get)
|
|
54
|
+
await device._check_connected()
|
|
55
|
+
assert device.connected is True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_check_connected_clears_flag_on_connection_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
60
|
+
device = AlpacaDevice(**_ARGS)
|
|
61
|
+
device._connected = True
|
|
62
|
+
|
|
63
|
+
async def fake_get(name: str) -> Any:
|
|
64
|
+
raise ConnectionError
|
|
65
|
+
|
|
66
|
+
monkeypatch.setattr(device, "_get", fake_get)
|
|
67
|
+
await device._check_connected()
|
|
68
|
+
assert device.connected is False
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Smoke tests: import every public module and instantiate each driver class with a
|
|
2
|
+
missing/mocked hardware connection, asserting it loads and advertises the interfaces it
|
|
3
|
+
claims.
|
|
4
|
+
|
|
5
|
+
No real hardware or ASCOM server is involved: instantiation only registers background
|
|
6
|
+
tasks, which never start until open() is called.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pyobs.interfaces import (
|
|
10
|
+
IDome,
|
|
11
|
+
IFitsHeaderBefore,
|
|
12
|
+
IFocuser,
|
|
13
|
+
IOffsetsRaDec,
|
|
14
|
+
ISyncTarget,
|
|
15
|
+
ITelescope,
|
|
16
|
+
)
|
|
17
|
+
from pyobs.object import Object
|
|
18
|
+
|
|
19
|
+
from pyobs_alpaca import AlpacaDome, AlpacaFocuser, AlpacaTelescope
|
|
20
|
+
from pyobs_alpaca.device import AlpacaDevice
|
|
21
|
+
|
|
22
|
+
_TELESCOPE_ARGS = dict(server="localhost", port=11111, device_type="telescope", device=0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_import_device_and_instantiate() -> None:
|
|
26
|
+
device = AlpacaDevice(**_TELESCOPE_ARGS)
|
|
27
|
+
assert isinstance(device, Object)
|
|
28
|
+
assert not device.connected
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_import_telescope_and_instantiate() -> None:
|
|
32
|
+
telescope = AlpacaTelescope(**_TELESCOPE_ARGS)
|
|
33
|
+
assert isinstance(telescope, ITelescope)
|
|
34
|
+
assert isinstance(telescope, IFitsHeaderBefore)
|
|
35
|
+
assert isinstance(telescope, IOffsetsRaDec)
|
|
36
|
+
assert isinstance(telescope, ISyncTarget)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_import_focuser_and_instantiate() -> None:
|
|
40
|
+
focuser = AlpacaFocuser(server="localhost", port=11111, device_type="focuser", device=0)
|
|
41
|
+
assert isinstance(focuser, IFocuser)
|
|
42
|
+
assert isinstance(focuser, IFitsHeaderBefore)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_import_dome_and_instantiate() -> None:
|
|
46
|
+
dome = AlpacaDome(server="localhost", port=11111, device_type="dome", device=0)
|
|
47
|
+
assert isinstance(dome, IDome)
|