pyobs-core 2.0.0.dev8__py3-none-any.whl → 2.0.0.dev10__py3-none-any.whl
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/comm/comm.py +14 -51
- pyobs/comm/local/localcomm.py +1 -6
- pyobs/comm/proxy.py +0 -6
- pyobs/comm/xmpp/rpc.py +1 -1
- pyobs/comm/xmpp/xmppcomm.py +53 -35
- pyobs/interfaces/IAutoFocus.py +28 -17
- pyobs/interfaces/IFitsHeaderAfter.py +2 -1
- pyobs/interfaces/IFitsHeaderBefore.py +9 -2
- pyobs/interfaces/IFocusModel.py +9 -4
- pyobs/interfaces/IWeather.py +21 -22
- pyobs/interfaces/__init__.py +11 -4
- pyobs/mixins/fitsheader.py +2 -7
- pyobs/mixins/fitsnamespace.py +4 -2
- pyobs/mixins/weatheraware.py +2 -1
- pyobs/modules/focus/focusmodel.py +15 -19
- pyobs/modules/focus/focusseries.py +14 -18
- pyobs/modules/module.py +1 -8
- pyobs/modules/pointing/_baseguiding.py +5 -5
- pyobs/modules/pointing/guidingstatistics/guidingstatistics.py +4 -2
- pyobs/modules/pointing/guidingstatistics/pixeloffset.py +5 -5
- pyobs/modules/pointing/guidingstatistics/skyoffsets.py +4 -4
- pyobs/modules/pointing/guidingstatistics/uptime.py +3 -3
- pyobs/modules/robotic/mastermind.py +4 -4
- pyobs/modules/roof/basedome.py +3 -3
- pyobs/modules/roof/baseroof.py +3 -3
- pyobs/modules/telescope/basetelescope.py +22 -15
- pyobs/modules/telescope/dummytelescope.py +3 -2
- pyobs/modules/weather/weather.py +46 -33
- pyobs/modules/weather/weather_state.py +1 -1
- pyobs/robotic/scripts/control/cases.py +3 -2
- pyobs/robotic/scripts/control/conditional.py +3 -2
- pyobs/robotic/scripts/imaging/imaging.py +4 -3
- pyobs/robotic/scripts/script.py +3 -2
- pyobs/robotic/storage/lco/scripts/default.py +4 -3
- pyobs/robotic/storage/lco/task.py +2 -1
- pyobs/robotic/task.py +2 -1
- pyobs/utils/focusseries/base.py +5 -0
- pyobs/utils/focusseries/photometry.py +5 -0
- pyobs/utils/focusseries/projection.py +5 -0
- pyobs/utils/gui/camera/datadisplaywidget.py +2 -2
- pyobs/utils/parallel.py +2 -17
- {pyobs_core-2.0.0.dev8.dist-info → pyobs_core-2.0.0.dev10.dist-info}/METADATA +1 -1
- {pyobs_core-2.0.0.dev8.dist-info → pyobs_core-2.0.0.dev10.dist-info}/RECORD +46 -47
- pyobs/utils/types.py +0 -242
- {pyobs_core-2.0.0.dev8.dist-info → pyobs_core-2.0.0.dev10.dist-info}/WHEEL +0 -0
- {pyobs_core-2.0.0.dev8.dist-info → pyobs_core-2.0.0.dev10.dist-info}/entry_points.txt +0 -0
- {pyobs_core-2.0.0.dev8.dist-info → pyobs_core-2.0.0.dev10.dist-info}/licenses/LICENSE +0 -0
pyobs/comm/comm.py
CHANGED
|
@@ -193,14 +193,25 @@ class Comm:
|
|
|
193
193
|
elif obj_type is None or isinstance(proxy, obj_type):
|
|
194
194
|
return proxy
|
|
195
195
|
else:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
message = f'Proxy obtained from given name "{name_or_object}" is not of requested type "{obj_type}".'
|
|
197
|
+
hint = self._diagnose_missing_interface(name_or_object, obj_type)
|
|
198
|
+
if hint is not None:
|
|
199
|
+
message += f" {hint}"
|
|
200
|
+
raise ValueError(message)
|
|
199
201
|
|
|
200
202
|
else:
|
|
201
203
|
# completely wrong...
|
|
202
204
|
raise ValueError(f'Given parameter is neither a name nor an object of requested type "{obj_type}".')
|
|
203
205
|
|
|
206
|
+
def _diagnose_missing_interface(self, client: str, obj_type: type[Any]) -> str | None:
|
|
207
|
+
"""Backend hook, called when a proxy exists but doesn't implement obj_type.
|
|
208
|
+
|
|
209
|
+
Backends that can tell "genuinely missing" apart from "present at an
|
|
210
|
+
incompatible version" using data already in hand return a hint string
|
|
211
|
+
to append to the ValueError; other backends just return None.
|
|
212
|
+
"""
|
|
213
|
+
return None
|
|
214
|
+
|
|
204
215
|
async def _safe_resolve_proxy(
|
|
205
216
|
self, name_or_object: str | object, obj_type: type[ProxyType] | None = None
|
|
206
217
|
) -> Any | ProxyType | None:
|
|
@@ -600,53 +611,5 @@ class Comm:
|
|
|
600
611
|
if asyncio.iscoroutine(ret):
|
|
601
612
|
asyncio.create_task(ret)
|
|
602
613
|
|
|
603
|
-
def cast_to_simple_pre(self, value: Any, annotation: Any | None = None) -> tuple[bool, Any]:
|
|
604
|
-
"""Special treatment of single parameters when converting them to be sent via Comm.
|
|
605
|
-
|
|
606
|
-
Args:
|
|
607
|
-
value: Value to be treated.
|
|
608
|
-
annotation: Annotation for value.
|
|
609
|
-
|
|
610
|
-
Returns:
|
|
611
|
-
A tuple containing a tuple that indicates whether this value should be further processed and a new value.
|
|
612
|
-
"""
|
|
613
|
-
return False, value
|
|
614
|
-
|
|
615
|
-
def cast_to_simple_post(self, value: Any, annotation: Any | None = None) -> tuple[bool, Any]:
|
|
616
|
-
"""Special treatment of single parameters when converting them to be sent via Comm.
|
|
617
|
-
|
|
618
|
-
Args:
|
|
619
|
-
value: Value to be treated.
|
|
620
|
-
annotation: Annotation for value.
|
|
621
|
-
|
|
622
|
-
Returns:
|
|
623
|
-
A tuple containing a tuple that indicates whether this value should be further processed and a new value.
|
|
624
|
-
"""
|
|
625
|
-
return False, value
|
|
626
|
-
|
|
627
|
-
def cast_to_real_pre(self, value: Any, annotation: Any | None = None) -> tuple[bool, Any]:
|
|
628
|
-
"""Special treatment of single parameters when converting them after being sent via Comm.
|
|
629
|
-
|
|
630
|
-
Args:
|
|
631
|
-
value: Value to be treated.
|
|
632
|
-
annotation: Annotation for value.
|
|
633
|
-
|
|
634
|
-
Returns:
|
|
635
|
-
A tuple containing a tuple that indicates whether this value should be further processed and a new value.
|
|
636
|
-
"""
|
|
637
|
-
return False, value
|
|
638
|
-
|
|
639
|
-
def cast_to_real_post(self, value: Any, annotation: Any | None = None) -> tuple[bool, Any]:
|
|
640
|
-
"""Special treatment of single parameters when converting them after being sent via Comm.
|
|
641
|
-
|
|
642
|
-
Args:
|
|
643
|
-
value: Value to be treated.
|
|
644
|
-
annotation: Annotation for value.
|
|
645
|
-
|
|
646
|
-
Returns:
|
|
647
|
-
A tuple containing a tuple that indicates whether this value should be further processed and a new value.
|
|
648
|
-
"""
|
|
649
|
-
return False, value
|
|
650
|
-
|
|
651
614
|
|
|
652
615
|
__all__ = ["Comm"]
|
pyobs/comm/local/localcomm.py
CHANGED
|
@@ -8,7 +8,6 @@ from pyobs.comm.comm import PresenceCallback
|
|
|
8
8
|
from pyobs.events import Event
|
|
9
9
|
from pyobs.interfaces import Interface
|
|
10
10
|
from pyobs.utils.enums import ModuleState
|
|
11
|
-
from pyobs.utils.types import cast_response_to_real
|
|
12
11
|
|
|
13
12
|
from .localnetwork import LocalNetwork
|
|
14
13
|
|
|
@@ -53,11 +52,7 @@ class LocalComm(Comm):
|
|
|
53
52
|
remote_client = self._network.get_client(client)
|
|
54
53
|
if remote_client.module is None:
|
|
55
54
|
raise ValueError
|
|
56
|
-
|
|
57
|
-
real_results = cast_response_to_real(
|
|
58
|
-
simple_results, annotation["return"], self.cast_to_real_pre, self.cast_to_real_post
|
|
59
|
-
)
|
|
60
|
-
return real_results
|
|
55
|
+
return await remote_client.module.execute(method, *args, sender=self.name)
|
|
61
56
|
|
|
62
57
|
async def send_event(self, event: Event) -> None:
|
|
63
58
|
"""Send an event to other clients."""
|
pyobs/comm/proxy.py
CHANGED
|
@@ -7,7 +7,6 @@ from collections.abc import Coroutine
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any, Generic, TypeVar, get_type_hints
|
|
8
8
|
|
|
9
9
|
from pyobs.interfaces import Interface
|
|
10
|
-
from pyobs.utils.types import cast_bound_arguments_to_simple
|
|
11
10
|
|
|
12
11
|
if TYPE_CHECKING:
|
|
13
12
|
from pyobs.comm import Comm
|
|
@@ -123,11 +122,6 @@ class Proxy:
|
|
|
123
122
|
ba = signature.bind(*args, **kwargs)
|
|
124
123
|
ba.apply_defaults()
|
|
125
124
|
|
|
126
|
-
# cast to simple types
|
|
127
|
-
cast_bound_arguments_to_simple(
|
|
128
|
-
ba, type_hints, pre=self._comm.cast_to_simple_pre, post=self._comm.cast_to_simple_post
|
|
129
|
-
)
|
|
130
|
-
|
|
131
125
|
# do request and return future
|
|
132
126
|
return await self._comm.execute(self._client, method, type_hints, *ba.args[1:])
|
|
133
127
|
|
pyobs/comm/xmpp/rpc.py
CHANGED
|
@@ -159,7 +159,7 @@ class RPC:
|
|
|
159
159
|
jid: str | slixmpp.JID = iq["id"]
|
|
160
160
|
if isinstance(jid, slixmpp.JID):
|
|
161
161
|
jid = jid.node
|
|
162
|
-
future = Future(annotation=annotation
|
|
162
|
+
future = Future(annotation=annotation)
|
|
163
163
|
self._futures[jid] = future
|
|
164
164
|
|
|
165
165
|
await iq.send()
|
pyobs/comm/xmpp/xmppcomm.py
CHANGED
|
@@ -18,6 +18,7 @@ from slixmpp.xmlstream import ET
|
|
|
18
18
|
from slixmpp.xmlstream.handler import Callback
|
|
19
19
|
from slixmpp.xmlstream.matcher import MatchXMLMask
|
|
20
20
|
|
|
21
|
+
import pyobs.interfaces
|
|
21
22
|
from pyobs.comm import Comm
|
|
22
23
|
from pyobs.events import Event, LogEvent, ModuleClosedEvent, ModuleOpenedEvent
|
|
23
24
|
from pyobs.events.event import EventFactory
|
|
@@ -144,6 +145,8 @@ class XmppComm(Comm):
|
|
|
144
145
|
self._connected = False
|
|
145
146
|
self._online_clients: list[str] = []
|
|
146
147
|
self._interface_cache: dict[str, asyncio.Future[list[type[Interface]]]] = {}
|
|
148
|
+
self._interface_features: dict[str, list[str]] = {}
|
|
149
|
+
self._warned_version_mismatches: set[tuple[str, str]] = set()
|
|
147
150
|
self._user = user
|
|
148
151
|
self._password = password
|
|
149
152
|
self._domain = domain
|
|
@@ -253,7 +256,7 @@ class XmppComm(Comm):
|
|
|
253
256
|
# add features
|
|
254
257
|
if self._module is not None:
|
|
255
258
|
for i in self._module.interfaces:
|
|
256
|
-
self._xmpp["xep_0030"].add_feature(f"pyobs:interface:{i.__name__}")
|
|
259
|
+
self._xmpp["xep_0030"].add_feature(f"urn:pyobs:interface:{i.__name__}:{i.version}")
|
|
257
260
|
|
|
258
261
|
# register custom disco#info handler to inject <capability> elements
|
|
259
262
|
if self._module is not None:
|
|
@@ -381,11 +384,24 @@ class XmppComm(Comm):
|
|
|
381
384
|
try:
|
|
382
385
|
if isinstance(info, slixmpp.stanza.iq.Iq):
|
|
383
386
|
info = info["disco_info"]
|
|
384
|
-
prefix = "pyobs:interface:"
|
|
385
|
-
|
|
387
|
+
prefix = "urn:pyobs:interface:"
|
|
388
|
+
features = [i for i in info["features"] if i.startswith(prefix)]
|
|
386
389
|
except TypeError:
|
|
387
390
|
raise IndexError()
|
|
388
391
|
|
|
392
|
+
# cache raw features for this JID, so a later version-mismatch can be diagnosed
|
|
393
|
+
self._interface_features[jid] = features
|
|
394
|
+
|
|
395
|
+
# keep only names whose remote-published version matches what this client expects --
|
|
396
|
+
# a mismatch is treated the same as the interface not being there at all, rather than
|
|
397
|
+
# silently using the local (possibly incompatible) class
|
|
398
|
+
interface_names = []
|
|
399
|
+
for feature in features:
|
|
400
|
+
name, _, version = feature[len(prefix) :].rpartition(":")
|
|
401
|
+
local_cls = getattr(pyobs.interfaces, name, None)
|
|
402
|
+
if local_cls is not None and issubclass(local_cls, Interface) and str(local_cls.version) == version:
|
|
403
|
+
interface_names.append(name)
|
|
404
|
+
|
|
389
405
|
# IModule not in list?
|
|
390
406
|
if "IModule" not in interface_names:
|
|
391
407
|
# try again or quit?
|
|
@@ -398,6 +414,39 @@ class XmppComm(Comm):
|
|
|
398
414
|
# finished
|
|
399
415
|
return interface_names
|
|
400
416
|
|
|
417
|
+
def _diagnose_missing_interface(self, client: str, obj_type: type[Any]) -> str | None:
|
|
418
|
+
"""Checks the disco#info features already cached for client for a version of obj_type
|
|
419
|
+
other than the one this client expects, to tell a version mismatch apart from
|
|
420
|
+
obj_type genuinely not being implemented at all.
|
|
421
|
+
"""
|
|
422
|
+
jid = self._get_full_client_name(client)
|
|
423
|
+
features = self._interface_features.get(jid, [])
|
|
424
|
+
prefix = f"urn:pyobs:interface:{obj_type.__name__}:"
|
|
425
|
+
other = [f for f in features if f.startswith(prefix)]
|
|
426
|
+
if not other:
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
remote_version = other[0][len(prefix) :]
|
|
430
|
+
try:
|
|
431
|
+
remote_version_int = int(remote_version)
|
|
432
|
+
except ValueError:
|
|
433
|
+
return None
|
|
434
|
+
direction = "upgrade the remote module" if remote_version_int < obj_type.version else "upgrade this client"
|
|
435
|
+
|
|
436
|
+
pair = (jid, obj_type.__name__)
|
|
437
|
+
if pair not in self._warned_version_mismatches:
|
|
438
|
+
self._warned_version_mismatches.add(pair)
|
|
439
|
+
log.warning(
|
|
440
|
+
'"%s" implements %s at v%s, this client expects v%s (%s).',
|
|
441
|
+
client,
|
|
442
|
+
obj_type.__name__,
|
|
443
|
+
remote_version,
|
|
444
|
+
obj_type.version,
|
|
445
|
+
direction,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
return f"Remote implements it at v{remote_version}, this client expects v{obj_type.version} ({direction})."
|
|
449
|
+
|
|
401
450
|
async def _supports_interface(self, client: str, interface: type[Interface]) -> bool:
|
|
402
451
|
"""Checks, whether the given client supports the given interface.
|
|
403
452
|
|
|
@@ -557,6 +606,7 @@ class XmppComm(Comm):
|
|
|
557
606
|
# clear interface cache
|
|
558
607
|
if jid in self._interface_cache:
|
|
559
608
|
del self._interface_cache[jid]
|
|
609
|
+
self._interface_features.pop(jid, None)
|
|
560
610
|
|
|
561
611
|
# send event
|
|
562
612
|
self._send_event_to_module(ModuleClosedEvent(), module_name)
|
|
@@ -730,38 +780,6 @@ class XmppComm(Comm):
|
|
|
730
780
|
# never should reach this
|
|
731
781
|
raise slixmpp.exceptions.IqTimeout(iq)
|
|
732
782
|
|
|
733
|
-
def cast_to_simple_pre(self, value: Any, annotation: Any | None = None) -> tuple[bool, Any]:
|
|
734
|
-
"""Special treatment of single parameters when converting them to be sent via Comm.
|
|
735
|
-
|
|
736
|
-
Args:
|
|
737
|
-
value: Value to be treated.
|
|
738
|
-
annotation: Annotation for value.
|
|
739
|
-
|
|
740
|
-
Returns:
|
|
741
|
-
A tuple containing a tuple that indicates whether this value should be further processed and a new value.
|
|
742
|
-
"""
|
|
743
|
-
|
|
744
|
-
if isinstance(value, str):
|
|
745
|
-
return True, xml.sax.saxutils.escape(value)
|
|
746
|
-
else:
|
|
747
|
-
return False, value
|
|
748
|
-
|
|
749
|
-
def cast_to_real_post(self, value: Any, annotation: Any | None = None) -> tuple[bool, Any]:
|
|
750
|
-
"""Special treatment of single parameters when converting them after being sent via Comm.
|
|
751
|
-
|
|
752
|
-
Args:
|
|
753
|
-
value: Value to be treated.
|
|
754
|
-
annotation: Annotation for value.
|
|
755
|
-
|
|
756
|
-
Returns:
|
|
757
|
-
A tuple containing a tuple that indicates whether this value should be further processed and a new value.
|
|
758
|
-
"""
|
|
759
|
-
|
|
760
|
-
if isinstance(value, str):
|
|
761
|
-
return True, xml.sax.saxutils.unescape(value)
|
|
762
|
-
else:
|
|
763
|
-
return False, value
|
|
764
|
-
|
|
765
783
|
@staticmethod
|
|
766
784
|
def _state_namespace(interface: type[Interface]) -> str:
|
|
767
785
|
return f"urn:pyobs:state:{interface.__name__}:{interface.version}"
|
pyobs/interfaces/IAutoFocus.py
CHANGED
|
@@ -1,24 +1,46 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import ABCMeta, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
4
5
|
from typing import Annotated, Any
|
|
5
6
|
|
|
6
7
|
from ..utils.enums import Unit
|
|
8
|
+
from ..utils.time import Time
|
|
7
9
|
from .IAbortable import IAbortable
|
|
8
10
|
|
|
9
11
|
|
|
12
|
+
@dataclass
|
|
13
|
+
class AutoFocusResult:
|
|
14
|
+
focus: float
|
|
15
|
+
focus_err: float
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AutoFocusPoint: # AutoFocusStatus.points element
|
|
20
|
+
focus: float
|
|
21
|
+
value: float
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AutoFocusState: # growing curve during autofocus run
|
|
26
|
+
points: list[AutoFocusPoint] = field(default_factory=list)
|
|
27
|
+
time: Time = field(default_factory=Time.now)
|
|
28
|
+
|
|
29
|
+
|
|
10
30
|
class IAutoFocus(IAbortable, metaclass=ABCMeta):
|
|
11
|
-
"""The module can perform an
|
|
31
|
+
"""The module can perform an autofocus."""
|
|
12
32
|
|
|
13
33
|
__module__ = "pyobs.interfaces"
|
|
14
34
|
|
|
35
|
+
state = AutoFocusState
|
|
36
|
+
|
|
15
37
|
@abstractmethod
|
|
16
38
|
async def auto_focus(
|
|
17
39
|
self, count: int, step: float, exposure_time: Annotated[float, Unit.SECONDS], **kwargs: Any
|
|
18
|
-
) ->
|
|
19
|
-
"""Perform an
|
|
40
|
+
) -> AutoFocusResult:
|
|
41
|
+
"""Perform an autofocus series.
|
|
20
42
|
|
|
21
|
-
This method performs an
|
|
43
|
+
This method performs an autofocus series with "count" images on each side of the initial guess and the given
|
|
22
44
|
step size. With count=3, step=1 and guess=10, this takes images at the following focus values:
|
|
23
45
|
7, 8, 9, 10, 11, 12, 13
|
|
24
46
|
|
|
@@ -28,23 +50,12 @@ class IAutoFocus(IAbortable, metaclass=ABCMeta):
|
|
|
28
50
|
exposure_time: Exposure time for images.
|
|
29
51
|
|
|
30
52
|
Returns:
|
|
31
|
-
|
|
53
|
+
Result of autofocus.
|
|
32
54
|
|
|
33
55
|
Raises:
|
|
34
56
|
ValueError: If focus could not be obtained.
|
|
35
57
|
"""
|
|
36
58
|
...
|
|
37
59
|
|
|
38
|
-
@abstractmethod
|
|
39
|
-
async def auto_focus_status(self, **kwargs: Any) -> dict[str, Any]:
|
|
40
|
-
"""Returns current status of auto focus.
|
|
41
|
-
|
|
42
|
-
Returned dictionary contains a list of focus/fwhm pairs in X and Y direction.
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
Dictionary with current status.
|
|
46
|
-
"""
|
|
47
|
-
...
|
|
48
|
-
|
|
49
60
|
|
|
50
|
-
__all__ = ["IAutoFocus"]
|
|
61
|
+
__all__ = ["IAutoFocus", "AutoFocusResult", "AutoFocusPoint", "AutoFocusState"]
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from abc import ABCMeta, abstractmethod
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from .IFitsHeaderBefore import FitsHeaderEntry
|
|
6
7
|
from .interface import Interface
|
|
7
8
|
|
|
8
9
|
|
|
@@ -15,7 +16,7 @@ class IFitsHeaderAfter(Interface, metaclass=ABCMeta):
|
|
|
15
16
|
@abstractmethod
|
|
16
17
|
async def get_fits_header_after(
|
|
17
18
|
self, namespaces: list[str] | None = None, **kwargs: Any
|
|
18
|
-
) -> dict[str,
|
|
19
|
+
) -> dict[str, FitsHeaderEntry]:
|
|
19
20
|
"""Returns FITS header for the current status of this module.
|
|
20
21
|
|
|
21
22
|
Args:
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import ABCMeta, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
from .interface import Interface
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
@dataclass
|
|
11
|
+
class FitsHeaderEntry:
|
|
12
|
+
value: int | float | str | None
|
|
13
|
+
comment: str
|
|
14
|
+
|
|
15
|
+
|
|
9
16
|
class IFitsHeaderBefore(Interface, metaclass=ABCMeta):
|
|
10
17
|
"""The module provides some additional header entries for FITS headers before some event (usually the start of the
|
|
11
18
|
exposure)."""
|
|
@@ -15,7 +22,7 @@ class IFitsHeaderBefore(Interface, metaclass=ABCMeta):
|
|
|
15
22
|
@abstractmethod
|
|
16
23
|
async def get_fits_header_before(
|
|
17
24
|
self, namespaces: list[str] | None = None, **kwargs: Any
|
|
18
|
-
) -> dict[str,
|
|
25
|
+
) -> dict[str, FitsHeaderEntry]:
|
|
19
26
|
"""Returns FITS header for the current status of this module.
|
|
20
27
|
|
|
21
28
|
Args:
|
|
@@ -27,4 +34,4 @@ class IFitsHeaderBefore(Interface, metaclass=ABCMeta):
|
|
|
27
34
|
...
|
|
28
35
|
|
|
29
36
|
|
|
30
|
-
__all__ = ["IFitsHeaderBefore"]
|
|
37
|
+
__all__ = ["FitsHeaderEntry", "IFitsHeaderBefore"]
|
pyobs/interfaces/IFocusModel.py
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
from abc import ABCMeta, abstractmethod
|
|
2
|
+
from dataclasses import dataclass, field
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
5
|
+
from ..utils.time import Time
|
|
4
6
|
from .interface import Interface
|
|
5
7
|
|
|
6
8
|
|
|
9
|
+
@dataclass
|
|
10
|
+
class OptimalFocusState: # 2.0 adds focus_err alongside the existing focus value
|
|
11
|
+
focus: float
|
|
12
|
+
time: Time = field(default_factory=Time.now)
|
|
13
|
+
|
|
14
|
+
|
|
7
15
|
class IFocusModel(Interface, metaclass=ABCMeta):
|
|
8
16
|
"""The module provides a model for the telescope focus, e.g. based on temperatures."""
|
|
9
17
|
|
|
10
18
|
__module__ = "pyobs.interfaces"
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
async def get_optimal_focus(self, **kwargs: Any) -> float:
|
|
14
|
-
"""Returns the optimal focus."""
|
|
15
|
-
...
|
|
20
|
+
state = OptimalFocusState
|
|
16
21
|
|
|
17
22
|
@abstractmethod
|
|
18
23
|
async def set_optimal_focus(self, **kwargs: Any) -> None:
|
pyobs/interfaces/IWeather.py
CHANGED
|
@@ -1,40 +1,39 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from abc import ABCMeta, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
from pyobs.utils.enums import WeatherSensors
|
|
7
8
|
|
|
9
|
+
from ..utils.time import Time
|
|
8
10
|
from .IStartStop import IStartStop
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
@dataclass
|
|
14
|
+
class WeatherSensorReading: # WeatherState.readings element
|
|
15
|
+
sensor: WeatherSensors
|
|
16
|
+
value: float
|
|
17
|
+
unit: str
|
|
18
|
+
time: Time = field(default_factory=Time.now)
|
|
13
19
|
|
|
14
|
-
__module__ = "pyobs.interfaces"
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
@dataclass
|
|
22
|
+
class WeatherState:
|
|
23
|
+
good: bool
|
|
24
|
+
readings: list[WeatherSensorReading] = field(default_factory=list)
|
|
25
|
+
time: Time = field(default_factory=Time.now)
|
|
20
26
|
|
|
21
|
-
@abstractmethod
|
|
22
|
-
async def is_weather_good(self, **kwargs: Any) -> bool:
|
|
23
|
-
"""Whether the weather is good to observe."""
|
|
24
|
-
...
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"""Returns current weather.
|
|
28
|
+
class IWeather(IStartStop, metaclass=ABCMeta):
|
|
29
|
+
"""The module acts as a weather station."""
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"""
|
|
34
|
-
...
|
|
31
|
+
__module__ = "pyobs.interfaces"
|
|
32
|
+
|
|
33
|
+
state = WeatherState
|
|
35
34
|
|
|
36
35
|
@abstractmethod
|
|
37
|
-
async def get_sensor_value(self, station: str, sensor: WeatherSensors, **kwargs: Any) ->
|
|
36
|
+
async def get_sensor_value(self, station: str, sensor: WeatherSensors, **kwargs: Any) -> WeatherSensorReading:
|
|
38
37
|
"""Return value for given sensor.
|
|
39
38
|
|
|
40
39
|
Args:
|
|
@@ -42,9 +41,9 @@ class IWeather(IStartStop, metaclass=ABCMeta):
|
|
|
42
41
|
sensor: Name of sensor to get value from.
|
|
43
42
|
|
|
44
43
|
Returns:
|
|
45
|
-
|
|
44
|
+
Current reading for the given sensor.
|
|
46
45
|
"""
|
|
47
46
|
...
|
|
48
47
|
|
|
49
48
|
|
|
50
|
-
__all__ = ["IWeather"]
|
|
49
|
+
__all__ = ["IWeather", "WeatherState", "WeatherSensorReading"]
|
pyobs/interfaces/__init__.py
CHANGED
|
@@ -13,7 +13,7 @@ __title__ = "Interfaces"
|
|
|
13
13
|
|
|
14
14
|
from .IAbortable import IAbortable
|
|
15
15
|
from .IAcquisition import IAcquisition
|
|
16
|
-
from .IAutoFocus import IAutoFocus
|
|
16
|
+
from .IAutoFocus import AutoFocusPoint, AutoFocusResult, AutoFocusState, IAutoFocus
|
|
17
17
|
from .IAutoGuiding import IAutoGuiding
|
|
18
18
|
from .IAutonomous import IAutonomous
|
|
19
19
|
from .IBinning import BinningCapabilities, BinningState, IBinning
|
|
@@ -27,10 +27,10 @@ from .IExposure import ExposureState, IExposure
|
|
|
27
27
|
from .IExposureTime import ExposureTimeState, IExposureTime
|
|
28
28
|
from .IFilters import FiltersCapabilities, FilterState, IFilters
|
|
29
29
|
from .IFitsHeaderAfter import IFitsHeaderAfter
|
|
30
|
-
from .IFitsHeaderBefore import IFitsHeaderBefore
|
|
30
|
+
from .IFitsHeaderBefore import FitsHeaderEntry, IFitsHeaderBefore
|
|
31
31
|
from .IFlatField import IFlatField
|
|
32
32
|
from .IFocuser import FocuserState, IFocuser
|
|
33
|
-
from .IFocusModel import IFocusModel
|
|
33
|
+
from .IFocusModel import IFocusModel, OptimalFocusState
|
|
34
34
|
from .IGain import GainState, IGain
|
|
35
35
|
from .IImageFormat import IImageFormat, ImageFormatCapabilities, ImageFormatState
|
|
36
36
|
from .IImageType import IImageType, ImageTypeState
|
|
@@ -58,13 +58,16 @@ from .ISyncTarget import ISyncTarget
|
|
|
58
58
|
from .ITelescope import ITelescope
|
|
59
59
|
from .ITemperatures import ITemperatures, SensorReading, TemperaturesState
|
|
60
60
|
from .IVideo import IVideo, VideoCapabilities
|
|
61
|
-
from .IWeather import IWeather
|
|
61
|
+
from .IWeather import IWeather, WeatherSensorReading, WeatherState
|
|
62
62
|
from .IWindow import IWindow, WindowCapabilities, WindowState
|
|
63
63
|
|
|
64
64
|
__all__ = [
|
|
65
65
|
"IAbortable",
|
|
66
66
|
"IAcquisition",
|
|
67
67
|
"IAutoFocus",
|
|
68
|
+
"AutoFocusResult",
|
|
69
|
+
"AutoFocusPoint",
|
|
70
|
+
"AutoFocusState",
|
|
68
71
|
"IAutoGuiding",
|
|
69
72
|
"IAutonomous",
|
|
70
73
|
"IBinning",
|
|
@@ -84,10 +87,12 @@ __all__ = [
|
|
|
84
87
|
"IFilters",
|
|
85
88
|
"FiltersCapabilities",
|
|
86
89
|
"FilterState",
|
|
90
|
+
"FitsHeaderEntry",
|
|
87
91
|
"IFitsHeaderAfter",
|
|
88
92
|
"IFitsHeaderBefore",
|
|
89
93
|
"IFlatField",
|
|
90
94
|
"IFocusModel",
|
|
95
|
+
"OptimalFocusState",
|
|
91
96
|
"IFocuser",
|
|
92
97
|
"FocuserState",
|
|
93
98
|
"IGain",
|
|
@@ -141,6 +146,8 @@ __all__ = [
|
|
|
141
146
|
"IVideo",
|
|
142
147
|
"VideoCapabilities",
|
|
143
148
|
"IWeather",
|
|
149
|
+
"WeatherState",
|
|
150
|
+
"WeatherSensorReading",
|
|
144
151
|
"IWindow",
|
|
145
152
|
"WindowCapabilities",
|
|
146
153
|
"WindowState",
|
pyobs/mixins/fitsheader.py
CHANGED
|
@@ -111,13 +111,8 @@ class FitsHeaderMixin:
|
|
|
111
111
|
# add them to fits file
|
|
112
112
|
if headers:
|
|
113
113
|
log.debug("Adding additional FITS headers from %s...", client)
|
|
114
|
-
for key,
|
|
115
|
-
|
|
116
|
-
if isinstance(value, list) and not isinstance(value, str):
|
|
117
|
-
# convert list to tuple
|
|
118
|
-
image.header[key] = tuple(value)
|
|
119
|
-
else:
|
|
120
|
-
image.header[key] = value
|
|
114
|
+
for key, entry in headers.items():
|
|
115
|
+
image.header[key] = (entry.value, entry.comment)
|
|
121
116
|
|
|
122
117
|
async def add_fits_headers(self, image: Image | fits.PrimaryHDU) -> None:
|
|
123
118
|
"""Add requested FITS headers to header of given image.
|
pyobs/mixins/fitsnamespace.py
CHANGED
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
from pyobs.interfaces import FitsHeaderEntry
|
|
7
|
+
|
|
6
8
|
log = logging.getLogger(__name__)
|
|
7
9
|
|
|
8
10
|
|
|
@@ -15,8 +17,8 @@ class FitsNamespaceMixin:
|
|
|
15
17
|
self.__namespaces = {} if fits_namespaces is None else fits_namespaces
|
|
16
18
|
|
|
17
19
|
def _filter_fits_namespace(
|
|
18
|
-
self, hdr: dict[str,
|
|
19
|
-
) -> dict[str,
|
|
20
|
+
self, hdr: dict[str, FitsHeaderEntry], sender: str, namespaces: list[str] | None = None
|
|
21
|
+
) -> dict[str, FitsHeaderEntry]:
|
|
20
22
|
"""Filter FITS header keywords by given namespaces. If no namespaces are given, let all through. Always
|
|
21
23
|
let keywords with this module's name as namespace pass.
|
|
22
24
|
|
pyobs/mixins/weatheraware.py
CHANGED
|
@@ -120,7 +120,8 @@ class WeatherAwareMixin:
|
|
|
120
120
|
try:
|
|
121
121
|
# get good status
|
|
122
122
|
async with module.proxy(this.__weather, IWeather) as proxy:
|
|
123
|
-
|
|
123
|
+
weather_state = await proxy.wait_for_state(IWeather, timeout=5.0)
|
|
124
|
+
this.__is_weather_good = weather_state.good if weather_state is not None else False
|
|
124
125
|
|
|
125
126
|
except Exception:
|
|
126
127
|
# could either not connect or weather is not good
|