pyobs-alpaca 1.1.2__tar.gz → 2.0.0.dev2__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.
Files changed (23) hide show
  1. pyobs_alpaca-2.0.0.dev2/.github/workflows/ruff.yml +23 -0
  2. pyobs_alpaca-2.0.0.dev2/.pre-commit-config.yaml +10 -0
  3. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/PKG-INFO +2 -2
  4. pyobs_alpaca-2.0.0.dev2/pyobs_alpaca/__init__.py +3 -0
  5. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/pyobs_alpaca/device.py +9 -16
  6. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/pyobs_alpaca/dome.py +39 -127
  7. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/pyobs_alpaca/focuser.py +31 -114
  8. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/pyobs_alpaca/telescope.py +92 -204
  9. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/pyproject.toml +16 -6
  10. pyobs_alpaca-2.0.0.dev2/uv.lock +2107 -0
  11. pyobs_alpaca-1.1.2/.pre-commit-config.yaml +0 -15
  12. pyobs_alpaca-1.1.2/pyobs_alpaca/__init__.py +0 -8
  13. pyobs_alpaca-1.1.2/uv.lock +0 -1508
  14. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/.github/workflows/pypi.yml +0 -0
  15. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/.readthedocs.yml +0 -0
  16. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/CHANGELOG.rst +0 -0
  17. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/LICENSE +0 -0
  18. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/README.md +0 -0
  19. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/docs/Makefile +0 -0
  20. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/docs/make.bat +0 -0
  21. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/docs/source/_static/pyobs.gif +0 -0
  22. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/docs/source/conf.py +0 -0
  23. {pyobs_alpaca-1.1.2 → pyobs_alpaca-2.0.0.dev2}/docs/source/index.rst +0 -0
@@ -0,0 +1,23 @@
1
+ name: ruff
2
+ on: push
3
+
4
+ jobs:
5
+ ruff:
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 ruff
23
+ run: uv run --no-sync ruff check pyobs_alpaca/
@@ -0,0 +1,10 @@
1
+ repos:
2
+ - repo: https://github.com/psf/black-pre-commit-mirror
3
+ rev: 25.1.0
4
+ hooks:
5
+ - id: black
6
+ language_version: python3.11
7
+ - repo: https://github.com/astral-sh/ruff-pre-commit
8
+ rev: v0.9.0
9
+ hooks:
10
+ - id: ruff
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyobs-alpaca
3
- Version: 1.1.2
3
+ Version: 2.0.0.dev2
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<2,>=1
10
+ Requires-Dist: pyobs-core<3,>=2.0.0.dev1
@@ -0,0 +1,3 @@
1
+ from .dome import AlpacaDome as AlpacaDome
2
+ from .focuser import AlpacaFocuser as AlpacaFocuser
3
+ from .telescope import AlpacaTelescope as AlpacaTelescope
@@ -1,8 +1,8 @@
1
1
  import asyncio
2
2
  import logging
3
3
  from typing import Any, NamedTuple
4
- import aiohttp
5
4
 
5
+ import aiohttp
6
6
  from pyobs.object import Object
7
7
 
8
8
  log = logging.getLogger("pyobs")
@@ -112,14 +112,7 @@ class AlpacaDevice(Object):
112
112
  Returns:
113
113
  Full Alpaca URL
114
114
  """
115
- return "http://%s:%d/api/%s/%s/%d/%s" % (
116
- self._server,
117
- self._port,
118
- self._version,
119
- self._type,
120
- self._device,
121
- name.lower(),
122
- )
115
+ return f"http://{self._server}:{self._port}/api/{self._version}/{self._type}/{self._device}/{name.lower()}"
123
116
 
124
117
  async def _get(self, name: str) -> Any:
125
118
  """Calls GET on Alpaca server, which returns value for variable with given name.
@@ -140,7 +133,7 @@ class AlpacaDevice(Object):
140
133
  # request it
141
134
  try:
142
135
  async with aiohttp.ClientSession() as session:
143
- async with session.get(url, timeout=5) as response:
136
+ async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as response:
144
137
  if response.status != 200:
145
138
  raise ConnectionError(
146
139
  f"ALPACA server responded with error {response.status}: {await response.text()}."
@@ -148,13 +141,13 @@ class AlpacaDevice(Object):
148
141
  json = await response.json()
149
142
  resp = ServerGetResponse(**json)
150
143
 
151
- except asyncio.TimeoutError:
144
+ except TimeoutError:
152
145
  # raise a ConnectionError instead
153
146
  raise ConnectionError("Connection to ALPACA server timed out.")
154
147
 
155
148
  # check error
156
149
  if resp.ErrorNumber != 0:
157
- raise ConnectionError("Server error: %s" % resp.ErrorMessage)
150
+ raise ConnectionError(f"Server error: {resp.ErrorMessage}")
158
151
 
159
152
  # return value
160
153
  return resp.Value
@@ -177,7 +170,7 @@ class AlpacaDevice(Object):
177
170
  raise ConnectionError("Not connected to ASCOM.")
178
171
  return await self._get(name)
179
172
 
180
- async def put(self, name: str, timeout: float = 5, **values: Any) -> None:
173
+ async def put(self, name: str, timeout: float = 5.0, **values: Any) -> None:
181
174
  """Calls PUT on Alpaca server with given variable, which might set a variable or call a method.
182
175
 
183
176
  Args:
@@ -199,7 +192,7 @@ class AlpacaDevice(Object):
199
192
  # request it
200
193
  try:
201
194
  async with aiohttp.ClientSession() as session:
202
- async with session.put(url, data=values, timeout=timeout) as response:
195
+ async with session.put(url, data=values, timeout=aiohttp.ClientTimeout(total=timeout)) as response:
203
196
  if response.status != 200:
204
197
  raise ConnectionError(
205
198
  f"ALPACA server responded with error {response.status}: {await response.text()}."
@@ -207,13 +200,13 @@ class AlpacaDevice(Object):
207
200
  json = await response.json()
208
201
  resp = ServerPutResponse(**json)
209
202
 
210
- except asyncio.TimeoutError:
203
+ except TimeoutError:
211
204
  # raise a ConnectionError instead
212
205
  raise ConnectionError("Connection to ALPACA server timed out.")
213
206
 
214
207
  # check error
215
208
  if resp.ErrorNumber != 0:
216
- raise ConnectionError("Server error: %s" % resp.ErrorMessage)
209
+ raise ConnectionError(f"Server error: {resp.ErrorMessage}")
217
210
 
218
211
 
219
212
  __all__ = ["AlpacaDevice"]
@@ -1,17 +1,18 @@
1
1
  import asyncio
2
2
  import logging
3
3
  import time
4
- from typing import Tuple, Optional, Any
4
+ from typing import Any
5
5
 
6
- from pyobs.events import RoofOpenedEvent, RoofClosingEvent
6
+ from pyobs.events import RoofClosingEvent, RoofOpenedEvent
7
+ from pyobs.interfaces import AltAzState, IPointingAltAz, IReady, ReadyState
7
8
  from pyobs.mixins import FollowMixin
8
- from pyobs.interfaces import IPointingAltAz
9
9
  from pyobs.modules import timeout
10
10
  from pyobs.modules.roof import BaseDome
11
+ from pyobs.utils import exceptions as exc
11
12
  from pyobs.utils.enums import MotionStatus
12
13
  from pyobs.utils.parallel import event_wait
13
14
  from pyobs.utils.threads import LockWithAbort
14
- from pyobs.utils import exceptions as exc
15
+
15
16
  from .device import AlpacaDevice
16
17
 
17
18
  log = logging.getLogger("pyobs")
@@ -20,8 +21,8 @@ log = logging.getLogger("pyobs")
20
21
  class AlpacaDome(FollowMixin, BaseDome):
21
22
  __module__ = "pyobs_alpaca"
22
23
 
23
- def __init__(self, tolerance: float = 3, park_az: float = 180, follow: Optional[str] = None, **kwargs: Any):
24
- """Initializes a new ASCOM Alpaca telescope.
24
+ def __init__(self, tolerance: float = 3, park_az: float = 180, follow: str | None = None, **kwargs: Any):
25
+ """Initializes a new ASCOM Alpaca dome.
25
26
 
26
27
  Args:
27
28
  tolerance: Tolerance for azimuth.
@@ -60,9 +61,8 @@ class AlpacaDome(FollowMixin, BaseDome):
60
61
  async def open(self) -> None:
61
62
  """Open module."""
62
63
  await BaseDome.open(self)
63
-
64
- # init status to IDLE
65
64
  await self._change_motion_status(MotionStatus.IDLE)
65
+ await self.comm.set_state(IReady, ReadyState(ready=self._device.connected))
66
66
 
67
67
  async def _send_open_dome(self) -> None:
68
68
  """Send command to open dome."""
@@ -74,40 +74,28 @@ class AlpacaDome(FollowMixin, BaseDome):
74
74
 
75
75
  @timeout(1200000)
76
76
  async def init(self, **kwargs: Any) -> None:
77
- """Open dome.
77
+ """Open dome."""
78
78
 
79
- Raises:
80
- pyobs.utils.exceptions.InitError: If dome cannot be opened.
81
- """
82
-
83
- # weather?
84
79
  if not self.is_weather_good():
85
80
  raise exc.InitError("Weather seems to be bad.")
86
81
 
87
- # if already opening, ignore
88
- if await self.get_motion_status() == MotionStatus.INITIALIZING:
82
+ if self.motion_status() == MotionStatus.INITIALIZING:
89
83
  return
90
84
 
91
- # acquire lock
92
85
  async with LockWithAbort(self._lock_shutter, self._abort_shutter):
93
- # log
94
86
  log.info("Opening dome...")
95
87
  await self._change_motion_status(MotionStatus.INITIALIZING)
96
88
 
97
- # execute command
98
89
  await self._send_open_dome()
99
90
  time_attempt = time.time()
100
91
 
101
- # wait for it
102
92
  status = None
103
93
  while status != 0:
104
- # error?
105
94
  if status == 4:
106
95
  log.error("Could not open dome.")
107
96
  await self._change_motion_status(MotionStatus.UNKNOWN)
108
97
  return
109
98
 
110
- # wait a little and update
111
99
  await event_wait(self._abort_shutter, 1)
112
100
  try:
113
101
  status = await self._device.get("ShutterStatus")
@@ -115,18 +103,16 @@ class AlpacaDome(FollowMixin, BaseDome):
115
103
  await self._change_motion_status(MotionStatus.UNKNOWN)
116
104
  raise exc.InitError("Could not open dome.")
117
105
 
118
- # waited >10s?
119
106
  if time.time() - time_attempt > 10:
120
107
  await self._send_open_dome()
121
108
  time_attempt = time.time()
122
109
 
123
- # set new status
124
110
  log.info("Dome opened.")
125
111
  await self._change_motion_status(MotionStatus.POSITIONED)
126
112
  await self.comm.send_event(RoofOpenedEvent())
127
113
 
128
114
  async def _send_close_dome(self) -> None:
129
- """Send command to open dome."""
115
+ """Send command to close dome."""
130
116
  try:
131
117
  await self._device.put("CloseShutter")
132
118
  await self._device.put("SlewToAzimuth", Azimuth=0)
@@ -136,37 +122,26 @@ class AlpacaDome(FollowMixin, BaseDome):
136
122
 
137
123
  @timeout(1200000)
138
124
  async def park(self, **kwargs: Any) -> None:
139
- """Close dome.
125
+ """Close dome."""
140
126
 
141
- Raises:
142
- pyobs.utils.exceptions.ParkError: If dome cannot be opened.
143
- """
144
-
145
- # if already closing, ignore
146
- if await self.get_motion_status() == MotionStatus.PARKING:
127
+ if self.motion_status() == MotionStatus.PARKING:
147
128
  return
148
129
 
149
- # acquire lock
150
130
  async with LockWithAbort(self._lock_shutter, self._abort_shutter):
151
- # log
152
131
  log.info("Closing dome...")
153
132
  await self._change_motion_status(MotionStatus.PARKING)
154
133
  await self.comm.send_event(RoofClosingEvent())
155
134
 
156
- # send command for closing shutter and rotate to South
157
135
  await self._send_close_dome()
158
136
  time_attempt = time.time()
159
137
 
160
- # wait for it
161
138
  status = None
162
139
  while status != 1:
163
- # error?
164
140
  if status == 4:
165
141
  log.error("Could not close dome.")
166
142
  await self._change_motion_status(MotionStatus.UNKNOWN)
167
143
  raise exc.ParkError("Could not close dome.")
168
144
 
169
- # wait a little and update
170
145
  await event_wait(self._abort_shutter, 1)
171
146
  try:
172
147
  status = await self._device.get("ShutterStatus")
@@ -174,41 +149,27 @@ class AlpacaDome(FollowMixin, BaseDome):
174
149
  await self._change_motion_status(MotionStatus.UNKNOWN)
175
150
  raise exc.ParkError("Could not close dome.")
176
151
 
177
- # waited >10s?
178
152
  if time.time() - time_attempt > 10:
179
153
  await self._send_close_dome()
180
154
  time_attempt = time.time()
181
155
 
182
- # set new status
183
156
  log.info("Dome closed.")
184
157
  await self._change_motion_status(MotionStatus.PARKED)
185
158
 
186
159
  async def _move(self, az: float, abort: asyncio.Event) -> None:
187
- """Move the roof and wait for it.
188
-
189
- Args:
190
- az: Azimuth to move to.
191
- abort: Abort event.
192
-
193
- Raises:
194
- pyobs.utils.exceptions.MoveError: If dome cannot be moved.
195
- """
160
+ """Move the dome to the given azimuth."""
196
161
 
197
- # execute command
198
162
  try:
199
163
  await self._device.put("SlewToAzimuth", Azimuth=self._adjust_azimuth(az))
200
164
  except ConnectionError:
201
165
  await self._change_motion_status(MotionStatus.UNKNOWN)
202
166
  raise exc.MoveError("Could not move dome.")
203
167
 
204
- # wait for it
205
168
  log_timer = 0
206
169
  while 180 - abs(abs(az - self._azimuth) - 180) > self._tolerance:
207
- # abort?
208
170
  if abort.is_set():
209
171
  raise InterruptedError("Moving dome aborted.")
210
172
 
211
- # log?
212
173
  if log_timer == 0:
213
174
  log.info(
214
175
  "Moving dome from current az=%.2f° to %.2f° (%.2f° left)...",
@@ -216,117 +177,68 @@ class AlpacaDome(FollowMixin, BaseDome):
216
177
  az,
217
178
  180 - abs(abs(az - self._azimuth) - 180),
218
179
  )
219
- log_timer += 1
220
- if log_timer == 10:
221
- log_timer = 0
180
+ log_timer = (log_timer + 1) % 10
222
181
 
223
- # wait a little
224
182
  await event_wait(abort, 1)
225
183
 
226
- # finished
227
184
  log.info("Moved to az=%.2f.", az)
228
185
 
229
186
  @timeout(1200000)
230
187
  async def move_altaz(self, alt: float, az: float, **kwargs: Any) -> None:
231
- """Moves to given coordinates.
188
+ """Moves to given coordinates."""
232
189
 
233
- Args:
234
- alt: Alt in deg to move to.
235
- az: Az in deg to move to.
236
-
237
- Raises:
238
- pyobs.utils.exceptions.MoveError: If device could not be moved.
239
- """
240
-
241
- # do nothing, if not ready
242
- if not await self.is_ready():
190
+ # do nothing if not ready
191
+ bad_states = [
192
+ MotionStatus.PARKED,
193
+ MotionStatus.INITIALIZING,
194
+ MotionStatus.PARKING,
195
+ MotionStatus.ERROR,
196
+ MotionStatus.UNKNOWN,
197
+ ]
198
+ if not self._device.connected or self.motion_status() in bad_states:
243
199
  return
244
200
 
245
- # destination az already set?
246
201
  if az == self._set_az:
247
202
  return
248
203
  self._set_az = az
249
204
 
250
- # is this a larger move?
251
205
  large_move = abs(az - self._azimuth) > 2.0 * self._tolerance
252
-
253
- # decide, whether we're tracking or just slewing
254
206
  tracking = self.is_following and not large_move
255
207
 
256
- # acquire lock
257
208
  async with LockWithAbort(self._lock_move, self._abort_move):
258
- # store altitude
259
209
  self._altitude = alt
260
-
261
- # change status to TRACKING or SLEWING, depending on whether we're tracking
262
210
  await self._change_motion_status(MotionStatus.TRACKING if tracking else MotionStatus.SLEWING)
263
-
264
- # move dome
265
211
  await self._move(az, self._abort_move)
266
-
267
- # change status to TRACKING or POSITIONED, depending on whether we're tracking
268
212
  await self._change_motion_status(MotionStatus.TRACKING if self.is_following else MotionStatus.POSITIONED)
269
213
 
270
- async def get_altaz(self, **kwargs: Any) -> Tuple[float, float]:
271
- """Returns current Alt and Az.
272
-
273
- Returns:
274
- Tuple of current Alt and Az in degrees.
275
- """
276
- return self._altitude, self._azimuth
277
-
278
- async def stop_motion(self, device: Optional[str] = None, **kwargs: Any) -> None:
279
- """Stop the motion.
280
-
281
- Args:
282
- device: Name of device to stop, or None for all.
283
- """
284
-
285
- # not supported, but don't want to raise an exception
214
+ async def stop_motion(self, device: str | None = None, **kwargs: Any) -> None:
215
+ """Stop the motion."""
286
216
  pass
287
217
 
288
- async def is_ready(self, **kwargs: Any) -> bool:
289
- """Returns the device is "ready", whatever that means for the specific device.
290
-
291
- Returns:
292
- Whether device is ready
293
- """
294
-
295
- # check that motion is not in one of the states listed below
296
- states = [
297
- MotionStatus.PARKED,
298
- MotionStatus.INITIALIZING,
299
- MotionStatus.PARKING,
300
- MotionStatus.ERROR,
301
- MotionStatus.UNKNOWN,
302
- ]
303
- return self._device.connected and await self.get_motion_status() not in states
304
-
305
218
  async def _update_status(self) -> None:
306
219
  """Update status from dome."""
307
-
308
- # loop forever
309
220
  while True:
310
- # get azimuth
311
221
  try:
312
222
  self._azimuth = self._adjust_azimuth(await self._device.get("Azimuth"))
223
+ await self.comm.set_state(IPointingAltAz, AltAzState(alt=self._altitude, az=self._azimuth))
313
224
  except ConnectionError:
314
- # ignore it
315
225
  pass
316
226
 
317
- # sleep a little
227
+ bad_states = [
228
+ MotionStatus.PARKED,
229
+ MotionStatus.INITIALIZING,
230
+ MotionStatus.PARKING,
231
+ MotionStatus.ERROR,
232
+ MotionStatus.UNKNOWN,
233
+ ]
234
+ ready = self._device.connected and self.motion_status() not in bad_states
235
+ await self.comm.set_state(IReady, ReadyState(ready=ready))
236
+
318
237
  await asyncio.sleep(2)
319
238
 
320
239
  @staticmethod
321
240
  def _adjust_azimuth(az: float) -> float:
322
- """Baader measures azimuth as West of South, so we need to convert it. This works both ways.
323
-
324
- Args:
325
- az: Azimuth.
326
-
327
- Returns:
328
- Converted azimuth.
329
- """
241
+ """Baader measures azimuth as West of South — convert both ways."""
330
242
  az += 180
331
243
  if az >= 360:
332
244
  az -= 360
@@ -1,14 +1,14 @@
1
1
  import asyncio
2
2
  import logging
3
- from typing import List, Dict, Tuple, Any, Optional
3
+ from typing import Any
4
4
 
5
- from pyobs.modules import Module
6
- from pyobs.interfaces import IFocuser, IFitsHeaderBefore
5
+ from pyobs.interfaces import FocuserState, IFitsHeaderBefore, IFocuser, IReady, ReadyState
7
6
  from pyobs.mixins import MotionStatusMixin
8
- from pyobs.modules import timeout
7
+ from pyobs.modules import Module, timeout
8
+ from pyobs.utils import exceptions as exc
9
9
  from pyobs.utils.enums import MotionStatus
10
10
  from pyobs.utils.threads import LockWithAbort
11
- from pyobs.utils import exceptions as exc
11
+
12
12
  from .device import AlpacaDevice
13
13
 
14
14
  log = logging.getLogger(__name__)
@@ -45,166 +45,83 @@ class AlpacaFocuser(MotionStatusMixin, IFocuser, IFitsHeaderBefore, Module):
45
45
 
46
46
  # init status
47
47
  await self._change_motion_status(MotionStatus.IDLE, interface="IFocuser")
48
+ await self.comm.set_state(IReady, ReadyState(ready=self._device.connected))
48
49
 
49
50
  async def init(self, **kwargs: Any) -> None:
50
- """Initialize device.
51
-
52
- Raises:
53
- pyobs.utils.exceptions.InitError: If device could not be initialized.
54
- """
51
+ """Initialize device."""
55
52
  pass
56
53
 
57
54
  async def park(self, **kwargs: Any) -> None:
58
- """Park device.
59
-
60
- Raises:
61
- pyobs.utils.exceptions.ParkError: If device could not be parked.
62
- """
55
+ """Park device."""
63
56
  pass
64
57
 
65
58
  async def get_fits_header_before(
66
- self, namespaces: Optional[List[str]] = None, **kwargs: Any
67
- ) -> Dict[str, Tuple[Any, str]]:
68
- """Returns FITS header for the current status of this module.
69
-
70
- Args:
71
- namespaces: If given, only return FITS headers for the given namespaces.
59
+ self, namespaces: list[str] | None = None, **kwargs: Any
60
+ ) -> dict[str, tuple[Any, str]]:
61
+ """Returns FITS header for the current status of this module."""
72
62
 
73
- Returns:
74
- Dictionary containing FITS headers.
75
- """
76
-
77
- # get pos and step size
78
- # StepSize is in microns, so multiply with 1000
79
63
  try:
80
64
  pos = await self._device.get("Position")
81
65
  step = await self._device.get("StepSize") * 1000.0
82
-
83
- # return header
84
66
  return {"TEL-FOCU": (pos / step, "Focus of telescope [mm]")}
85
-
86
67
  except ConnectionError as e:
87
68
  log.warning("Could not determine focus position: %s", e)
88
69
  return {}
89
70
 
90
71
  @timeout(60000)
91
72
  async def set_focus(self, focus: float, **kwargs: Any) -> None:
92
- """Sets new focus.
93
-
94
- Raises:
95
- pyobs.utils.exceptions.MoveError: If telescope cannot be moved.
96
- """
97
-
98
- # set focus + offset
99
-
73
+ """Sets new focus."""
100
74
  await self._set_focus(focus + self._focus_offset)
101
75
 
102
76
  async def set_focus_offset(self, offset: float, **kwargs: Any) -> None:
103
- """Sets focus offset.
77
+ """Sets focus offset."""
104
78
 
105
- Args:
106
- offset: New focus offset.
107
-
108
- Raises:
109
- InterruptedError: If focus was interrupted.
110
- """
111
-
112
- # get current focus (without offset)
113
- focus = await self.get_focus()
79
+ # get current focus (without offset) directly from device
80
+ try:
81
+ pos = float(await self._device.get("Position"))
82
+ step = float(await self._device.get("StepSize")) * 1000.0
83
+ current_focus = pos / step - self._focus_offset
84
+ except ConnectionError:
85
+ raise exc.MoveError("Could not read focus position.")
114
86
 
115
- # set offset
116
87
  self._focus_offset = offset
117
-
118
- # go to focus
119
- await self._set_focus(focus + self._focus_offset)
88
+ await self._set_focus(current_focus + self._focus_offset)
120
89
 
121
90
  async def _set_focus(self, focus: float) -> None:
122
- """Actually sets new focus.
91
+ """Actually sets new focus."""
123
92
 
124
- Args:
125
- focus: New focus value.
126
- """
127
-
128
- # acquire lock
129
93
  async with LockWithAbort(self._lock_motion, self._abort_motion):
130
94
  try:
131
- # get step size
132
95
  step = await self._device.get("StepSize")
133
96
 
134
- # calculating new focus and move it
135
97
  log.info("Moving focus to %.2fmm...", focus)
136
98
  await self._change_motion_status(MotionStatus.SLEWING, interface="IFocuser")
137
99
  foc = int(focus * step * 1000.0)
138
100
  await self._device.put("Move", Position=foc)
139
101
 
140
- # wait for it
141
102
  while abs(await self._device.get("Position") - foc) > 10:
142
- # abort?
143
103
  if self._abort_motion.is_set():
144
104
  await self._device.put("Halt")
145
105
  await self._change_motion_status(MotionStatus.POSITIONED, interface="IFocuser")
146
106
  raise InterruptedError("Setting focus aborted.")
147
-
148
- # sleep a little
149
107
  await asyncio.sleep(0.1)
150
108
 
151
- # finished
152
- log.info("Reached new focus of %.2fmm.", await self._device.get("Position") / step / 1000.0)
109
+ pos = await self._device.get("Position")
110
+ log.info("Reached new focus of %.2fmm.", pos / step / 1000.0)
153
111
  await self._change_motion_status(MotionStatus.POSITIONED, interface="IFocuser")
154
112
 
113
+ # publish new state
114
+ await self.comm.set_state(
115
+ IFocuser, FocuserState(focus=pos / step / 1000.0, focus_offset=self._focus_offset)
116
+ )
117
+
155
118
  except ConnectionError:
156
119
  await self._change_motion_status(MotionStatus.ERROR, interface="IFocuser")
157
120
  raise exc.MoveError("Could not move focus.")
158
121
 
159
- async def get_focus(self, **kwargs: Any) -> float:
160
- """Return current focus.
161
-
162
- Returns:
163
- Current focus.
164
- """
165
-
166
- # get pos and step size
167
- # StepSize is in microns, so multiply with 1000
168
- pos = float(await self._device.get("Position"))
169
- step = float(await self._device.get("StepSize")) * 1000.0
170
-
171
- # return current focus - offset
172
- return pos / step - self._focus_offset
173
-
174
- async def get_focus_offset(self, **kwargs: Any) -> float:
175
- """Return current focus offset.
176
-
177
- Returns:
178
- Current focus offset.
179
- """
180
- return self._focus_offset
181
-
182
- async def stop_motion(self, device: Optional[str] = None, **kwargs: Any) -> None:
183
- """Stop the motion.
184
-
185
- Args:
186
- device: Name of device to stop, or None for all.
187
- """
188
-
189
- # stop motion
122
+ async def stop_motion(self, device: str | None = None, **kwargs: Any) -> None:
123
+ """Stop the motion."""
190
124
  await self._device.put("Halt")
191
125
 
192
- async def is_ready(self, **kwargs: Any) -> bool:
193
- """Returns the device is "ready", whatever that means for the specific device.
194
-
195
- Returns:
196
- True, if telescope is initialized and not in an error state.
197
- """
198
-
199
- # check that motion is not in one of the states listed below
200
- states = [
201
- MotionStatus.PARKED,
202
- MotionStatus.INITIALIZING,
203
- MotionStatus.PARKING,
204
- MotionStatus.ERROR,
205
- MotionStatus.UNKNOWN,
206
- ]
207
- return self._device.connected and await self.get_motion_status() not in states
208
-
209
126
 
210
127
  __all__ = ["AlpacaFocuser"]