Changelog
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Entries for 2.6.0 to 2.14.0 were generated from commit subjects. Older entries were converted from the former CHANGELOG.rst.
[2.14.0] - 2026-10-04
Slim base dependencies for pyobs-gui, move the rest to the full extra
BaseVideo: opt-in CORS for /video.raw (cors_origins)
[2.13.5] - 2026-10-04
Add VirtualFileSystem.set_roots() to replace roots at runtime (#939)
docs: add pyobs-auth to the project tiers doc
[2.13.4] - 2026-10-04
Resolve stretch cuts from the input dtype, carry SRCDTYPE in raw frame meta
Log missing science exptimes in darkbias at info level
[2.13.3] - 2026-09-29
fix: AstroplanScheduler hanging forever when scheduled blocks can’t be pickled
[2.13.2] - 2026-09-29
fix: end video streams quietly on any client disconnect, show IQ failure reason in subscribe warnings
docs: refresh fleet open items (2026-09-29)
[2.13.1] - 2026-09-29
Export Frame, FrameRecord, StartSource from pyobs.modules.camera
[2.13.0] - 2026-09-29
specs: BaseVideo redesign merged (PR #926), drop #924/#925 from fleet-open-items
BaseVideo: frame buffer, timestamped frames, new grab path, stream guiding (#926)
specs: BaseVideo redesign design docs and plan (#924, #925)
steering: remove closed pyobs-web-client #40-#47 from fleet-open-items
steering: record the IResettable pyobs-core pin bug and its fleet-wide fix
steering: remove IResettable driver-override issues from fleet-open-items
specs: mark all eight IResettable sibling-repo issues as implemented
[2.12.0] - 2026-09-27
CHANGELOG: Image.writeto() NAXIS fix (#922)
Image.writeto(): let astropy regenerate NAXIS cards
Add stack combine processors (MeanStack, MedianStack, SumStack, SigmaClipStack) (#921)
Remove finished items from fleet-open-items (open items only)
Update fleet-open-items snapshot for 2026-09-27
Fix stale status line on data-pipeline-stack-reset plan doc
Link phase 8 driver issues in iresettable.md and the plan doc
[2.11.0] - 2026-09-27
Add IResettable, IDataPipeline, IDataStack to BaseCamera/BaseVideo (#919)
Fall back to selected pipeline when BaseVideo image has no pipeline meta
Add design docs and plan for IResettable, IDataPipeline and IDataStack
[2.10.7] - 2026-09-23
Harden PushNotifier against FCM payload and outage failure modes
[2.10.6] - 2026-09-20
Send valid LCO config states to the portal
[2.10.5] - 2026-09-19
Fix ResolvableErrorLogger’s min_interval comparison and bookkeeping
Log a failing archive query as a throttled warning, not a traceback
Add ArchiveError and raise it for every archive failure
[2.10.4] - 2026-09-19
fix: let class-passed child objects inherit vfs/timezone/observer
[2.10.3] - 2026-09-18
ImageWatcher: default error_after to 1800s instead of requiring it
docs: mark push-notification preferences client half done
[2.10.2] - 2026-09-18
feat: rename IPushNotifications methods, add get_push_preferences
[2.10.1] - 2026-09-18
feat: per-user push notification type preferences in PushNotifier
docs: re-check fleet open items (2026-09-18)
[2.10.0] - 2026-09-17
feat: extract DataSequenceMixin, add IDataSequence support to BaseSpectrograph
docs: mark imagewatcher-retry-hardening landed, add pyobs-web-client#57
[2.9.2] - 2026-09-17
fix: harden ImageWatcher retry behavior on failed file processing
docs: mark push-notification-module implemented, update fleet-open-items
[2.9.1] - 2026-09-15
fix: run firebase_admin.delete_app() in a thread in PushNotifier.close()
docs: qfitswidget toolbar-overflow fix released (qfitswidget 1.1.3, pyobs-gui 2.4.2)
[2.9.0] - 2026-09-14
docs: sync fleet-open-items.md changelog
feat: implement PushNotifier module (module ERROR + log ERROR/CRITICAL to FCM)
Drop two stale pyobs-web-client entries from fleet open items
docs: fold ERROR/CRITICAL log alerts into PushNotifier v1, file #902
docs: pyobs-gui scroll-fallback plan implemented, drop from open plans
docs: track two new pyobs-gui plans; update telescopewidget-layout status
docs: drop web-admin#95 – already fixed, released in v2.3.4, closed
docs: close #859 as not worth it, drop from fleet tracker
docs: reject gui-standalone-binary – real build came out several GB
docs: close #899 – released in v2.8.9
[2.8.9] - 2026-09-14
docs: record #899 fix merged to develop, drop from fleet tracker
Route ACL-denial faults through the normal call_id/fault path (#899)
Track pyobs-brot#71 (settle-timeout root cause) in fleet tracker
Record BROT settle-loop staleness/resend plan; close pyobs-brot#61 and #68 in fleet tracker
chore: untrack .claude/, keep it local-only
[2.8.8] - 2026-09-14
docs: close #898, #846, #884; fleet maintenance pass
feat: publish struct field schemas in disco#info’s
block Surface call_id in ShellCommand’s remote-error formatting
docs: close pyobs-web-client #48/#49, cross-file call_id issue (#54/pyobs-gui#167)
docs: record pyobs-pipeline’s Keycloak login cutover (#17)
docs: move iag50 2.x migration plan to pyobs-iag50’s own specs
docs: close #896, mark general per-exptime-darks-on-lco-sites plan accepted
docs: split #896 design into general pyobs-core plan + site-specific
docs: cross-reference iag50cm per-exptime-darks plan for #896
docs: drop closed #891 from fleet snapshot
docs: drop landed #895 from fleet snapshot, mark plan merged
fix: reschedule when mastermind skips a late start window (#897)
removed graphify
docs: refresh fleet open-items snapshot (2026-09-09)
[2.8.7] - 2026-09-09
feat: return per-run success/failure counts from Reduction.call
fix: pass a Quantity dark_exposure to ccdproc even when dark_scale=False
Sketch a PushNotifier module design for issue #884’s deferred push notifications
[2.8.6] - 2026-09-08
fix: publish DummyCamera capabilities after connecting, not before
[2.8.5] - 2026-09-08
fix: re-raise weather station errors so poll loop backs off
specs/steering: update fleet-open-items snapshot to 2026-09-08
[2.8.4] - 2026-09-08
fix: tolerate unknown fields in LCO portal API responses
[2.8.3] - 2026-09-08
fix: publish caps hash update when XMPP capabilities are set
[2.8.2] - 2026-09-08
fix: register MoveRaDecEvent/MoveAltAzEvent in BaseTelescope.open()
docs: fix stale pyobs-web-client plan links in fleet-open-items
[2.8.1] - 2026-09-07
xmpp: add loopback ws listener on 5282, off the public path
Mark mobile-app RN design superseded by pyobs-web-client Capacitor plan
Record XMPP-core spike outcome: stanza.js chosen (issue #884)
Add mobile-app design doc + ADRs 0016-0018 (mobile client, shared TS core)
Drop pyobs-weather#6 from fleet open items: merged to develop
Update fleet steering docs: close blocking-SDK punch list, refresh open items
[2.8.0] - 2026-09-04
Address review: min_length on module_name, structural-failure test, policy note
Add camera/filter-wheel model fields, fix nullable module_name bug
[2.7.2] - 2026-09-04
feat: add description to ConfigFieldSchema
[2.7.1] - 2026-09-04
feat: propagate AccessLevel through ConfigFieldSchema
[2.7.0] - 2026-09-04
feat: add AccessLevel enum for GUI basic/expert config field tiers
Add RoofCapability for plain open/close roof timing
Add mean-distance dome rotate time to first-task duration estimates
docs: close out pyobs-core#871 in fleet open-items snapshot
fix: address review nits on Comm.unregister_event() cancellation (#871)
fix: Comm.unregister_event() cancels already-scheduled handler tasks (#871)
docs: close out pyobs-core#872 in fleet open-items snapshot
docs: close out #872 fits-header-audit-followthrough plan as implemented
[2.6.1] - 2026-09-03
Fix FilterHeaderMixin/FocuserHeaderMixin crash when chaining to an abstract get_fits_header_before
docs: record 2.6.0 release + sibling pin-bump status in fits-header-audit plan
docs: plan for Comm.unregister_event() task-cancellation fix (#871)
[2.6.0] - 2026-09-03
Add FITS header fields for camera flip/gain, telescope tracking mode, guiding offsets, and weather verdict; add FilterHeaderMixin/FocuserHeaderMixin
docs: drop closed pyobs-core#739 from fleet open-items snapshot
docs: drop closed pyobs-core#863 from fleet open-items snapshot
docs: drop pyobs-gui main-vs-sidebar-widgets + video-widget-split from fleet open-items
docs: drop closed pyobs-web-admin#89 from fleet open-items snapshot
[2.5.0] - 2026-09-03
New
IResettableinterface (reset()/full_reset()): a module can reset its per-acquisition settings (exposure time, image type, binning, window, data pipeline) or, withfull_reset(), everything including hardware state (cooling). Implemented byBaseCamera,BaseVideoandBaseSpectrograph.Module.startup()now callsfull_reset()automatically for any module implementing it, and the robotic scripts (Imaging,DarkBiasScript, the LCO default script,FlatField) callreset()before configuring a camera for a new task. Seespecs/design/iresettable.md.New
IDataPipelineinterface (set_pipeline()): a camera can run a named, YAML-configured pipeline on every grab before storing it – the pipeline result replaces the raw data, and every stored file carries aPIPELINEFITS header. Implemented byBaseCameraandBaseVideovia the newDataPipelineMixin, configured with newpipelines/default_pipelineinit parameters. Seespecs/design/idatapipeline.md.New
IDataStackinterface (grab_stack(count)): grabscountconsecutive frames into one 3D cube product (or, with a pipeline selected, its combined result), blocking until the product is stored. Implemented byBaseCameraandBaseVideo, memory-capped via newmax_stack_bytes/stack_frame_overhead/stack_timeout_margininit parameters. Seespecs/design/idatastack.md.New
pyobs.images.processors.stackpackage: pipeline steps that collapse agrab_stack()cube into one float32 frame, one class per method (MeanStack,MedianStack,SumStack,SigmaClipStack) on a commonCombineStackbase. Non-stack images pass through unchanged, NaNs are ignored, the cube is processed in row blocks in a worker thread, and an uncertainty and mask extension can be enabled. Seespecs/design/combinestack.md.Image: fixedNAXIS1/NAXIS2for 3D data (previously swapped),is_colornow excludes stack cubes (CTYPE3 == 'FRAME'), and a new optionalframestable (per-frameFRAME/DATE-OBS/EXPTIME, written/read as aFRAMESFITS extension). Also fixesBaseCamera.apply_meridian_flip()flipping the wrong axes on 3D data. Seespecs/design/idatastack.md.Image.writeto(): no longer fails on misplaced or staleNAXISncards (e.g. a header passed toImage()without NAXIS cards, orNAXIS3left over after collapsing a cube via thedatasetter). They are now regenerated from the data on write (#922).Every module implementing
IFitsHeaderBefore/IFitsHeaderAfternow writes oneHIERARCH <MODULE> VERSION <PACKAGE>card per loadedpyobs-*package into FITS headers, reusingloaded_pyobs_packages()(added for #759). Closes #739. Seespecs/design/package_versions_fits_header.md.Fixes
Comm.unregister_event()not cancelling a handler dispatch that_send_event_to_module()had already scheduled viaasyncio.create_task()before the unregister call – the task ran later regardless, against whatever the handler was bound to (observed in pyobs-gui as alibshiboken: Internal C++ object already deletederror on client disconnect). Closes #871.
[2.4.1] - 2026-09-03
Fixes
Time.night_obs()flipping to the next night around dawn: it anchored the night boundary at “nearest sunset + 12h”, which lands close to sunrise near the equinox – exactly when morning calibration scripts run (e.g.DarkBiasScriptqueried the following night, with no science frames yet, instead of the one that just ended). Anchored at local solar noon instead, matching the archive’s own night-labeling convention and not drifting with day length. Also affectsMastermind’s night resolution and theDAY-OBSFITS header, which share the same call.
[2.4.0] - 2026-09-03
Breaking:
TaskScheduler.schedule()(bothOnDemandSchedulerandAstroplanScheduler) gained a newinstrument_capabilities: InstrumentCapabilities | None = Noneparameter, part of pyobs-core#864/#865’s plumbing for feeding pyobs-portal instrument capability data into script duration estimates (seespecs/plans/2026-09-01-instrument-capability-duration-estimates.md). Any out-of-treeTaskSchedulersubclass overridingschedule(tasks, projects, start, end)without the new keyword argument will raiseTypeErroronce called bypyobs/modules/robotic/scheduler.py’s_schedule_worker, which now always passes it. Add the parameter to your override’s signature (a default ofNoneis enough if you don’t use it) to stay compatible.Added
InstrumentCapabilities(pyobs/robotic/instruments.py), mirroring pyobs-portal’s instrument JSON with module-name-keyed camera/telescope/dome/filter-wheel capability lookups, and threaded it throughTaskData/TaskArchive/Task.estimate_duration().PortalTaskArchivenow polls and caches real capability data fromGET /api/instruments/(re-fetched only when pyobs-portal’slast_instrument_updatemarker moves in either direction; a fetch/parse failure keeps serving the last-good data rather than blocking the tasks/projects poll).ImagingScript,PointingScript,DarkBiasScript, andAutoFocusScriptnow use real portal-declared readout/filter-change/slew numbers where available, falling back to the previous flat fudge constants otherwise. Seespecs/plans/2026-09-01-instrument-capability-duration-estimates.md.Fixes the
Scheduler’sget_schedule()/get_current_observation()permanently returning an empty schedule: forcingauto_update=Falseon the observation archive also silenced the poller that’s the only channel by which the scheduler process learns about observation-state changes written by other processes (e.g.Mastermindmarking an observationIN_PROGRESS/COMPLETED). Addedannounce_updatestoPortalObservationArchiveinstead, so the poller can stay on without double-logging its own writes.VFSgainedrmdir()(LocalFile/VirtualFileSystem);ImageWatchernow removes now-empty parent directories left behind when it deletes a watched file.ImageWatcheronly attempts FITS header parsing for filenames that look like FITS (.fits/.fitz/.fits.gz/.fits.fz); other watched/copied files no longer produce spurious astropy header-parse warnings.
[2.3.0] - 2026-09-02
ImageWatchergained aflatten=True(default) constructor option: setFalseto keep a non-templated destination file’s path relative towatchpathinstead of collapsing it to just the basename, so a whole nested directory tree can be relocated while preserving its structure. Also added recursive watching (inotify mode via asyncinotify’sRecursiveWatcher, poll mode, and the initialopen()scan), so subdirectories are picked up too, not just the watched root. Fixed two pre-existing bugs while rewriting the poll loop:poll_intervalwas stored but never actually awaited between iterations (a busy-loop), and a stray debugprint()was left in the new-file branch. (#860)Issue #855:
PolymorphicBaseModel’s custom serializer (used byTask/Script/Constraint/Merit/Targetand their subclasses) now honorsexclude/include/by_alias/exclude_none/exclude_unset/exclude_defaultsonmodel_dump()/model_dump_json()instead of silently ignoring them, for flat top-level field specs (the only pattern any current caller uses); a nested spec now raisesNotImplementedErrorinstead of being silently ignored or partially applied.Issue #844:
Reduction’s minimum-darks-per-group threshold is now configurable viamin_darks_per_group(default 3, unchanged), matching the existingmin_flatsoption.Issue #856:
PortalTaskArchive._update()no longer counts a bareupdated_atbump (e.g. a no-op re-save) as a real content change, which previously triggered an avoidable re-download and reschedule.Issue #851:
DummyCameragained an optionalroofreference (mirroring the existingtelescopereference) and now skips flat-field/star simulation, producing dark frames, while itsDummyRoofis parked; fails closed until the roof’s state has been received.Issue #848:
Scheduler._update_schedule()now also reschedules on a project priority change or a same-ID task content change (e.g. priority), not just on task IDs appearing or disappearing; also fixes an assignment-order bug whereself._projectswas overwritten before it could ever be diffed. Follow-up review fixes: preserve the portal task FK when canceling (the cancel PUT was silently rejected every time otherwise), guard against a startup race between the observation- and task-archive pollers, and stopget_current_observation()from self-healing an in-progress observation out from under a runningMastermind.Issue #850:
AltitudeLimitErrorno longer inherits fromMotionError, so refusing a below-horizon target no longer counts toward motion-fault escalation (3 legitimate rejections within 10 minutes used to push the module intoERRORstate, requiring a manualreset_error()).Issue #845:
Module._on_module_openedandFocusSeries._run_focus_seriesnow also catch the plainValueErrorthatComm._resolve_proxy()raises (per its documented contract) when a proxy can’t be resolved, instead of leaking it to the caller.Issue #847:
Scheduler._update_schedule()no longer skips rescheduling on portal task removal (the observation-archive schedule-cache gate it relied on is permanently empty forPortalObservationArchiveby construction).PortalObservationArchivealso self-heals observations whose task the portal no longer resolves, canceling them locally and on the portal instead of logging-and-skipping forever.Issue #849:
DummyRoof.stop_motion()now reportsPARKEDrather thanIDLEon a closed roof, matchinginit()/park().
[2.2.0] - 2026-09-01
Issue #832:
Calibration/Reductionnow match dark masters to a science frame’sEXPTIMEinstead of always scaling whatever dark masterfind_masterhappened to return (see ADR0015-dark-master-strict-exptime-matching-reference-scale-down-only.md). An exact exptime match (withindark_exptime_tolerance, default 1%) is used unscaled; belowdark_min_exptime(default 5s) with no exact match, calibration falls back to bias-only; a reference master (dark_scale_exptime, default 600s) is scaled down to shorter science exptimes but never scaled up.Behavior change, upgrade check required: a site that takes darks at only one exptime and previously relied on always-scale-whatever’s-nearest now fails calibration (a caught, catchable
ValueError, logged and returned uncalibrated – not a crash) for any science exptime longer than its one master, or with no reference configured. Setallow_unmatched_dark_scale=Trueto keep today’s behavior, or take darks at more than one exptime (DarkBiasScript’sexptimes/match_science_exptimes, issue #831) so exact matches exist.Reduction’s master-calibration-frame filename pattern also gained an exposure-time component ({EXPTIME|exptime}, shared by BIAS/SKYFLAT too); existing archives keep their old-pattern masters under their original filenames.A reference master is also no longer scaled up (only down), and a legacy master with no
EXPTIMEin its header is now used unscaled instead of raisingKeyErroron theallow_unmatched_dark_scalefallback path.Issue #831:
DarkBiasScriptcan now take one morning dark series per exposure time actually used by the night’s science frames, instead of always a single fixed exptime. Addsscience_exptimes_for_night()(derives per-instrument/binning exptimes from the night’sOBJECTframes; exptimes belowdark_min_exptimeare dropped since calibration treats them as bias-only) and exposesEXPTIMEon the archive API (FrameInfo,list_frames/list_options, bothPyobsArchiveandLocalArchive) so it can be queried. Enable viaDarkBiasScript’s newexptimes/match_science_exptimesoptions.Added
DummyStructuredConfig(pyobs.modules), implementingIStructuredConfigwith one field of everyConfigFieldSchematype (str, int, a float with a unit, bool, enum, nested object) so pyobs-gui’s newStructuredConfigWidget(gui#154) has something to manually verify against without pyobs-iagvt’s FTS, the only otherIStructuredConfigconsumer.Fixes issue #830:
http_request_with_retriesno longer logs a WARNING on every failed retry attempt. Retries now stay quiet for the first 60s a URL has been failing (covering a typical short pyobs-portal restart), then warn at most once per minute until the request succeeds again.PortalTaskArchive/PortalObservationArchive’s poll loops got the same treatment for theirlog.errorcalls: the first failure still logs immediately at ERROR, but repeats during the same outage are throttled to at most once per minute instead of once per ~20s poll cycle. Both reuse a newpyobs.utils.http.LogThrottlehelper.
[2.1.0] - 2026-08-31
Added
IRobotic(executor) /IRoboticScheduler(planner) interfaces, wired intoMastermindandSchedulerrespectively (issue #825).IRoboticpushesRoboticState(current task, next-up,cant_run_reason) on every transition;IRoboticScheduleraddsget_schedule(limit), returning pending/in-progress entries only, capped server-side regardless of the requested limit.TaskStartedEvent/TaskFinishedEvent/TaskFailedEventnow also carryobsnum. Seespecs/design/irobotic.md.Added
DummyMastermind/DummyScheduler(pyobs.modules.robotic), simulating a robotic schedule so pyobs-gui’s newRoboticWidget/ScheduleWidgethave real state transitions to develop and test against without live hardware.XmppCommnow pre-creates a module’s own event pubsub nodes at the moment it declares them (send-onlyregister_event()), instead of relying on the server’s lazy auto-create-on-first-publish. Closes the window where a peer connecting before a module’s first publish could never complete a subscribe (item-not-found) and had to fall back to an indefinitely retrying background task. State nodes are explicitly out of scope (seespecs/plans/2026-08-28-precreate-pubsub-nodes.md’s “Why state nodes are excluded” section).Fixes issue #824:
_retry_delay’s exponential backoff no longer raisesOverflowErrorat high attempt counts (e.g. attempt 1024), and both event/state subscribe-retry background tasks now discard their tracked key/handler on an unexpected failure instead of leaving a permanently stuck “subscribed” marker behind with nothing actually subscribed and nothing retrying.Fixes issue #829:
DummyCameraexposures were broken under the pinned photutils 3.0 –make_model_imagelooks up source positions by the model’s own parameter names (x_0/y_0forMoffat2D), not by the sources table’s column names (x_mean/y_mean); now passesparams_mapto bridge the two.
[2.0.0] - 2026-08-26
v2.0.0.dev98 (2026-08-25)
Observationgained anarchive_urlfield (str | None, defaultNone), matching the computed link pyobs-portal#82 started returning fromGET /api/observations/(a deep link into the archive’s frontend for terminal observations; alwaysNonefor thepending/in_progressstates Mastermind schedules on). Without it, the strictObservationmodel rejected the new key andPortalObservationArchivefailed every download; the validation error was swallowed by_check_for_changes’s retry handler, so the marker-gated poll loop stalled forever without ever picking up schedule changes. The field is portal-only UI metadata that Mastermind tolerates but never consumes.
v2.0.0.dev97 (2026-08-25)
No pyobs-core code changes – version bump only.
v2.0.0.dev96 (2026-08-25)
Breaking:
pyobs.robotic.storage.backendis renamed topyobs.robotic.storage.portal, andBackendTaskArchive/BackendObservationArchiveare renamed toPortalTaskArchive/PortalObservationArchive, following thepyobs-robotic-backend->pyobs-portalrename (see ADR 0013,specs/adrs/0013-renaming-pyobs-robotic-backend.md). There is no deprecation shim – update theclass:dotted path in any deployment YAML using the old names before upgrading.
v2.0.0.dev95 (2026-08-25)
Added
Field(description=...)to every field across allpyobs.robotic.scriptsScriptsubclasses and their nested config models (e.g. skyflat priorities) – pyobs-robotic-backend’s script builder renders each field’s JSON-schema description as form help text, and none had one (InstrumentConfig.image_typeonly appeared to, via a leakedImageTypeenum docstring, now replaced with an explicit member listing). A new parametrized test guards everyScript/config field for a description going forward. Fixes #811. (#812)
v2.0.0.dev94 (2026-08-24)
Fixed broken
__module__overrides onPolymorphicBaseModelsubclasses (SkyFlatsBasePointing,SkyflatPriorities,Archiveand its subclasses includingLocalArchive/PyobsArchive): a stale short path left over from an earlierpyobs.robotic.utilsreorg made serialization, which keys onself.__module__, produce unparseable dumps on reload (ModuleNotFoundError). Removed the overrides so__module__reflects the real, importable path. Fixes #806.BaseVideo.raw_handlernow caches the per-frame(meta, frame)bytes it builds (header assembly, JSON serialization,ascontiguousarray/tobytes()copy), keyed by_frame_num. With N simultaneous raw clients (guiding, a recorder, a viewer – a case the raw endpoint (specs/design/basevideo-raw-frame-streaming.md) already supports but didn’t dedupe for), only the first consumer to wake for a given frame does the work; the rest reuse the cached result instead of redoing it (#769). Costs nothing with 0 or 1 raw clients connected.Mastermind.get_fits_header_before()now emits aPROJECTFITS keyword fromTask.project, alongsideTASK/REQNUM/OBSNUM– closes the last open item of the project-based access-control work for pyobs-archive (pyobs-archive#42; superseded by that repo’s own more detailed plan, which already shipped the archive-side filtering).Scriptmodule-name fields (pyobs.robotic.scripts) are now tagged with their requiredpyobs.interfacesviaAnnotatedmetadata, letting pyobs-robotic-backend introspectFieldInfo.metadatato render interface-filtered module dropdowns instead of hand-maintaining a duplicate table (pyobs-robotic-backend#98). Fixes #808. (#809)
v2.0.0.dev93 (2026-08-23)
ImageWatcherno longer blocks the event loop while processing watched files: per-file FITS parsing (astropy.io.fits.HDUList.fromstring) now runs viaasyncio.to_thread(), and all blockingLocalFileI/O (open/read/write/close/remove/find/exists) is routed through the default executor, mirroring the existinglistdirpattern – root cause of the 2026-08-20 “Event loop stalled” incident on MONET South, where RPC handling, inotify processing, and comm keepalive froze for seconds during a slow parse.LocalFile’sopen()/makedirsmoved from__init__into__aenter__(still validating the path synchronously at construction time), closing a gap for network-mounted watch paths; every VFS consumer inherits the fix, not justImageWatcher. Seespecs/plans/2026-08-20-imagewatcher-event-loop-blocking.md. (#798)move_radec/move_altaznow reject non-finite (NaN/inf) RA/Dec/Alt/Az up front withInvalidArgumentError, instead of letting them reach a NaN altitude comparison that was silently alwaysFalseand thenAngle.to_string(), whose numpy 2.x vectorized formatter raised aRuntimeWarningthatapplication.py’s filterwarnings promotes to a hard error –killing the RPC handler mid-slew. Allto_string()call sites in the affected module were replaced with manual sexagesimal formatting, mirroring pyobs-gui’s fix for the same numpy 2.x issue. Fixes #802.
v2.0.0.dev92 (2026-08-23)
BaseVideogained an opt-intokenparameter gating its MJPEG/raw/FITS endpoints behind either anAuthorization: Bearer <token>header or an HMAC-signed session cookie issued by a new browser/loginpage (/logoutclears it);/pingstays open. The cookie is stateless (expiry plus HMAC-SHA256 over the expiry, keyed by the token), all comparisons are constant-time, and an unauthenticated request is redirected (401 -> 303) to/loginso browsers land on the form.HttpFilegained a publicheadersproperty so consumers (e.g. pyobs-gui’sVideoWidget) can read theAuthorizationheader without reaching into a private attribute. Seespecs/plans/2026-08-21-basevideo-http-token-auth.md. (#799)
v2.0.0.dev91 (2026-08-21)
Demoted routine retry/shutdown log messages from WARNING to INFO.
Fixed
BIASSEC/TRIMSECFITS-header computation: the old guard fired (and warned) for any window not covering the full visible frame on both axes – exactly the common case of a subframe with no prescan/overscan at all (e.g. guided skyflats) – while the case it claimed to describe, prescan/overscan on both axes, slipped through silently and wrote an incomplete one-axisBIASSEC. The check is now based on whether the window actually extends beyond the visible frame: fully-inside windows log at INFO (nothing to compute), and true both-axis prescan/overscan windows warn that only one axis is supported. Header output is unchanged for the common cases.Applicationnow logs module-creation failures (unconsumed config keys, broken__init__chains, …) at ERROR level with the full traceback before re-raising, so they land in the configured log file / journald instead of only on stderr – which is/dev/nullfor daemonized modules (--pid-file, as started by pyobsd / pyobs-web-admin). Such a failure previously vanished from all logs: the log simply stopped after the last INFO line and the module just showed as stopped.Fixed
BaseVideo’s index page using a root-absolute/video.mjpgimage URL, which 404s when the module is served behind a reverse-proxy path prefix (e.g./pyobs/fibercamera/) since the browser resolves it against the host root instead of the page’s own path – and since that request never reaches the module, the camera is never (re)activated. Now relative.
v2.0.0.dev90 (2026-08-20)
BackendTaskArchiveandBackendObservationArchivegate their 5s refresh on the backend’slast_task_update/last_observation_updatemarker again. The marker is now a DB-derivedMax(updated_at)(pyobs-robotic-backend#84) that is truthful across gunicorn workers, so the archives no longer re-download (and re-compare) on every poll – the per-poll content comparison was fragile and misfired whenever runtime code mutated a serialized field (see theDynamicTarget.resolve()fix below), livelocking the scheduler. The content comparison is kept as theon_tasks_changedtrigger, so no-op marker bumps never fire a scheduler run.
v2.0.0.dev89 (2026-08-20)
DynamicTarget.resolve()no longer overwrites its declarednamefield with the picked star’s name. That mutation leaked intoTask.model_dump()(which serializes the static target), soBackendTaskArchive’s content comparison saw a “change” on every poll while the scheduler resolved a dynamic target (e.g. the CSV-picker autofocus task) – the constanton_tasks_changedkept_need_updateset and every scheduler run aborted itself with “Not using scheduler results, since update was requested”, so no schedule was ever committed. The picked star is still available via the resolved target (_target) and lands in the scheduled observation; the serializednamestays"(dynamic)".
v2.0.0.dev88 (2026-08-20)
Taskgained anupdated_atfield (str | None, defaultNone), matching the field pyobs-robotic-backend#84 started returning fromGET /api/tasks/(DB-derivedupdated_atonTask). Without it, the strictTaskmodel rejected the new key andBackendTaskArchivefailed every download, leaving the scheduler and mastermind without a task list.
v2.0.0.dev87 (2026-08-20)
BackendTaskArchiveandBackendObservationArchiveno longer gate their 5s refresh loop on the robotic-backend’slast_task_update/last_observation_updatemarker. That marker is computed from a per-process DjangoLocMemCacheand is unreliable across gunicorn workers, so a stale or missing marker pinned the archive’s_last_updateand the mastermind kept running stale tasks/schedules (edited task parameters, deactivated tasks, newly scheduled observations, window-expired observations) forever. Both archives now re-fetch unconditionally on every poll and detect real changes by comparing the downloaded content against the cached copy (full model dumps, including observationstate), firingon_tasks_changedonly when content actually changed;last_changed()now reports the local time a change was last observed. Fixes #789. (#790)PyobsArchiveandLocalArchivegained anobsnumfilter onlist_frames()andlist_options():PyobsArchivenow emits theOBSNUMquery parameter (exact match onFrame.OBSNUM), andLocalArchivefilters its local index on theOBSNUMFITS header, so observations can be matched to their archived frames vialist_frames(obsnum=...). Needed by pyobs-robotic-backend#82. (#791)
v2.0.0.dev86 (2026-08-19)
Fixed
FitsHeaderMixin.add_requested_fits_headers()letting a misbehaving peer’s malformedIFitsHeaderBefore/IFitsHeaderAfterresponse crash the whole exposure: it only caughtexc.RemoteError, butXmppComm.execute()only convertsIqError/IqTimeoutinto that, so anything else raised while making or deserializing the RPC call escaped uncaught. Now catchesExceptionbroadly (logging the peer and failure type, skipping just that peer), whileCancelledError– aBaseException– still propagates, preserving cancellation semantics. Fixes #767.Projectgained apublicflag (defaultFalse), matching the field the robotic backend (pyobs-robotic-backend#79) will start returning fromGET /api/projects/. Without it, the strictProjectmodel rejected the new key andBackendTaskArchivesilently kept running on stale or empty task/project data. Fixes #786.Fixed
XmppCommcrashing on payload-less pubsub notifications (retract stanzas, node purges, and nodes withdeliver_payloadsoff all arrive without a<payload>element under<item>):_handle_eventraisedAttributeErroronNone.text, surfacing only as a noisy “Task exception was never retrieved” since the dispatching task was never awaited. Now guards the payload access and drops payload-less notifications, and the background task gets a done-callback so any future exception is retrieved and logged normally.
v2.0.0.dev85 (2026-08-19)
http_request_paginated()now tolerates page drift mid-fetch: DRF’spage=Npagination 404s with “Invalid page.” onceNexceeds the current page count, and on a live-growing dataset with non-unique ordering (e.g.Observation.Meta.ordering = ["start"]) a concurrent insert can shift page boundaries between fetching a “next” link and following it, crashing the whole request. Now stops pagination early with whatever was already fetched – but only for that specific error shape, and never on the first page, so other 404s still raise.
v2.0.0.dev84 (2026-08-19)
Projectgained a missingusersfield (per-user project visibility), matching the robotic backend’sProjectSerializer. Withextra="forbid"enabled (#762), the unmodeled field previously raised a validation error instead of being silently dropped.
v2.0.0.dev83 (2026-08-19)
The
TypeErrorraised when a leftover kwarg reachesobject.__init__()unclaimed (end of the cooperativesuper().__init__(**kwargs)chain added below) is now re-raised with a diagnosable message naming the offending class and the actual unconsumed kwargs – notObject’s own pre-consumption view, which could wrongly flag a kwarg a downstream mixin in the MRO had already legitimately claimed (e.g.motion_status_interfaces). The precise leftover set is read from the deepest frame in the chained exception’s traceback, i.e. the cooperative call that actually reachedobject.__init__()and got rejected. Previously this raised only the generic, class-and-kwarg-lessobject.__init__() takes exactly one argumentmessage.
v2.0.0.dev82 (2026-08-19)
Object.__init__now forwards leftover kwargs cooperatively viasuper().__init__(**kwargs)instead of silently absorbing them, giving mixins listed afterModulein a subclass’s MRO (WeatherAwareMixin,MotionStatusMixin,PipelineMixin, etc.) a real chance to claim their own kwargs – prerequisite for eventually raising on genuinely unrecognized ones. Converted the 14 pyobs-core classes that composed mixins via explicit multi-call fan-out to a single cooperative call, reordering base classes inBaseRoof/BaseTelescope/DummyModesoModuleruns first (every mixin that callsadd_background_task/add_child_objector readsself.commneedsObject/Modulealready set up).FitsHeaderMixin’s cache-path attribute is now a lazily-computed property instead of eager at__init__time, since it can now run beforeModulehas setself._device_name.PipelineMixin’sarchivedefault-injection now only applies to step classes that actually declare anarchiveparameter (checked via signature inspection), instead of injecting unconditionally and relying on silent-drop – exactly the behavior this change removes. Seespecs/plans/2026-08-18-cooperative-mixin-init.md.Fixed a regression from the change above: unconditionally dropping
auto_update=FalsefromScheduler’s kwarg injection (justified at the time as “never declared, no effect”) silently re-enabledBackendObservationArchive’s 5-second polling loop, which does declare and gate on that kwarg – a poller deliberately disabled when the scheduler moved to event-drivenon_tasks_changedupdates, live on two fleet configs (MONET South, iagvtsrv). The kwarg-support check is now a general_class_accepts_param(), reused for both theobservation_archiveandauto_updateinjections.Fixed
WindowingWidget.value_top(pyobs/utils/gui/camera/windowingwidget.py) returning the Left spinbox’s value instead of the Top spinbox’s (copy-paste bug) – windowing offsets set via the GUI applied the wrong Y coordinate.
v2.0.0.dev81 (2026-08-18)
No pyobs-core code changes – version bump only.
v2.0.0.dev80 (2026-08-18)
BaseVideonow declares itself a sender ofNewImageEventviacomm.register_event()inopen(), matchingBaseCamera’s existing registration – without it, a peer’s disco#info-driven subscription logic never subscribed (since it skips event types a peer doesn’t advertise sending), sosend_event()succeeded server-side but nothing ever received it: the GUI’s video/FITS grab silently never updated, with no errors on either side.
v2.0.0.dev79 (2026-08-18)
Fixed a TOCTOU race in
BaseVideo.activate_camera()/deactivate_camera(): two concurrent callers (e.g. an incoming video-stream request racing an RPC likeset_exposure_time) could both observeself._activeas unset and both proceed to open/close the camera, double-connecting to the same GigE device and tripping “Controller privilege required for streaming control” on devices that only grant one controller. The check-and-set is now serialized with a lock.
v2.0.0.dev78 (2026-08-18)
BaseVideogained a raw-frame streaming endpoint (/video.raw) alongside the existing MJPEG stream: a multipart, event-driven feed of a JSON FITS-keyed meta header plus raw little-endian frame bytes, with latest-frame-wins backpressure (a slow client drops stale frames instead of queuing).VideoCapabilities.videois renamed tomjpeg, and a newrawfield (str | None) advertises the raw endpoint’s path;live_viewcollapses intovideo_pathalongside the newraw_path.add_fits_headers()is split intoadd_local_fits_headers()(no VFS I/O, usable per-frame) plus the persistentFRAMENUMstep. Also fixedvideo_handler’s hardcoded 1 fps interval and lowered the defaultsleep_timeto 60s. A connected-but-idle raw client now keeps the camera awake correctly:raw_handlerbounds its wait for a new frame with a timeout and re-touches the activity timestamp even when no frame arrives, instead of only doing so when a frame is actually sent.DATE-OBSis captured at acquisition time (in_set_image()) rather than at send time, avoiding drift under frame coalescing or scheduling delay. Seespecs/design/2026-08-11-basevideo-raw-frame-streaming.md. (#766, #770)Bounded
add_requested_fits_headers()’s wait for peer-supplied FITS headers with a singleasyncio.wait()deadline (newfits_header_timeoutkwarg onFitsHeaderMixin, default 15s), instead of awaiting every header future with no timeout at all – a peer that never answers its IQ (e.g. a laptop put to sleep without closing its client) previously stalled frame finalization for the full ~120s XMPP IQ timeout. Fixed two bugs found while implementing this:asyncio.wait()raisedValueErroron an empty futures dict (any module with no comm, or no peer implementingIFitsHeaderBefore/IFitsHeaderAfter), crashing every exposure; andBaseCamera/BaseVideopassed an explicit keyword list toImageFitsHeaderMixin.__init__instead of forwarding**kwargs, silently dropping a configuredfits_header_timeoutfor exactly the modules (e.g.fli230) that motivated this fix. Fixes #764. (#765, #768)
v2.0.0.dev77 (2026-08-16)
Every module now logs the versions of loaded
pyobs-*packages at startup, alongside the existing IERS-cache priming. Guarded with the same try/except as that priming, so broken dist-info metadata on any installed package can’t abort startup; an editable install won’t show up in the output (packages_distributions()can’t derive its top-level-name mapping withoutRECORDfile entries), which is fine since the only consumer only needs this in production. (#759)Fixed
WeatherAwareMixinretryingpark()on every 10s bad-weather check while a device was already inMotionStatus.ERROR, instead of only once: the error-state guard only suppressed the first post-errorpark()call, then fell through and called it again on every subsequent check. Devices whosepark()raises onERRORstatus (BrotRoof,BrotTelescope) flooded logs with aParkErrorplus “Task exception was never retrieved” every cycle, since the call happens via a fire-and-forget background task.
v2.0.0.dev76 (2026-08-15)
BaseCameranow publishes liveIExposurestate (progress/exposure_time_left) once a second whileEXPOSING, via a new background task; previously it only published on status transitions, so both values stayed frozen at 0 for the whole exposure.exposure_time_leftis clamped to>= 0.
v2.0.0.dev75 (2026-08-15)
BaseVideo’s image-cache write now offloadsimage.to_bytes()to a worker thread (matching the existingcreate_jpeg()pattern), instead of serializing a ~5MB FITS frame directly on the event loop – same class of fix asVfs.write_image()/write_fits()in dev53, applied to a different choke point.
v2.0.0.dev74 (2026-08-15)
FocusModel’s coefficient fit (lmfit.minimizeplus residual model evaluation) now runs on a worker thread instead of directly on the event loop.
v2.0.0.dev71 (2026-08-10)
Added
OBSNUM:Mastermindnow assigns a compound<night>-<counter>observation number when a task starts running, persisted via a VFS-cached per-night counter and written to bothObservation.obsnumand theOBSNUMFITS header – lets frames from possibly-multiple cameras be tied back to the observation that produced them. Seespecs/design/obsnum_fits_header.md. (#738)
v2.0.0.dev70 (2026-08-10)
Added structured progress reporting to
Reduction(aprogress_callbackreporting master-calib creation and per-frame science calibration results as they happen, with a cumulative whole-night frame total computed via a cheap pre-pass), so a caller (e.g.pyobs-pipeline’s web UI) can drive a live progress bar instead of only tailing free-text logs.Reductionis split into an abstractReductionBase(archive/pipeline plumbing, master-calib cache/lookup, the progress-callback mechanism) and the concreteReduction, giving a future second reduction strategy a base to build on.PipelineMixinnow propagates aPipeline’s ownarchiveinto any step that doesn’t specify its own –Reductionalready passedarchive=archiveintoget_object(pipeline, Pipeline, archive=archive), clearly intending it to reach steps likeCalibration, butPipeline.__init__absorbed it intoObject.__init__(**kwargs)and dropped it, so everyCalibrationstep had to redundantly repeat the same archive config already set at theReduction/site level. An explicit per-steparchivestill overrides it.Fixed
Calibrationaborting when a science frame arrived with a catalog already attached (e.g. quick-look photometry done at the telescope): it trippedImage.trim()’s stale-catalog guard, even thoughto_ccddata()never carries a catalog through anyway. The pre-existing catalog is now dropped before trimming.Made
AperturePhotometryabstract instead of exporting it as if instantiable directly – it’s a base class forPhotUtilsPhotometry/SepPhotometry(each supplies its own calculator), and its__module__override advertised a dotted path the package’s__init__.pynever actually re-exported. Breaking change for any external code instantiatingAperturePhotometrydirectly (subclasses that define their own__init__, as both existing ones already do, are unaffected).Removed the generated TypeScript interface exports (
export/typescript/) – breaking change for any external tooling consuming them.
v2.0.0.dev68 (2026-08-09)
Renamed
pyobs.utils.pipeline.NighttoReduction(fits solar telescopes too, not just nighttime ones). Replacedstore_localwith a singleoutputparam (local path, dict, orArchive), so input and output can be different archives. ImplementedLocalArchive.upload_frames(previously a silent no-op), including aValueErroron a missingFNAMEheader and a_update_root()refresh after write; the local output directory is now auto-created if missing. Calibration-frame creation failures are now isolated per instrument/binning/filter combination instead of aborting the whole run. Fixed a missingreturn Noneafter the post-download frame-count check. Removed the deadworker_procsparam and the**kwargscatch-all onReduction.__init__, so a typo’d kwarg now raisesTypeErrorinstead of being silently swallowed. Breaking change for any external code usingNight,store_local, orworker_procsdirectly.
v2.0.0.dev67 (2026-08-06)
Re-landed the
OnDemandSchedulerevent-loop offload (added in dev53, briefly reverted in dev66) unchanged, alongside a prefetch/freeze split forObservationArchiveEvolution: a newprefetch()fetches every task’s observations plus the one real “current” night (night(start)) up front, thenfreeze()makes a subsequent task-id cache miss raiseRuntimeErrorinstead of silently falling back to a live archive lookup, while a miss on any other (necessarily later, necessarily empty) night seeds an empty result instead of fetching or raising. Groundwork for eventually moving that evaluation from a worker thread to a worker process (real GIL isolation); the thread-shared live archive connection was the blocker, since a process pool can’t share it. Seespecs/plans/scheduler-archive-prefetch-for-process-isolation.md.
v2.0.0.dev66 (2026-08-05)
Briefly reverted the
OnDemandSchedulerevent-loop offload added in dev53 while investigating the process-isolation groundwork above; re-landed unchanged in dev67, so only a dev66 build itself ran evaluation synchronously on the event loop again.Mastermindnow logs a task failing with a domainPyobsError(e.g.AcquisitionError) at INFO instead of ERROR with a full traceback – these are expected outcomes, not bugs; only a genuinely unclassified exception still logs loudly.
v2.0.0.dev65 (2026-08-05)
Fixed a scheduler event-loop stall traced live (
py-spy dump) to the main thread, not the worker thread the dev53 offload targets:ObservationArchiveEvolution.evolve()calledTime.now().night_obs()directly on the event loop for every scheduled task, redoing an uncached astropy sunset computation each time – a 52-block schedule triggered this dozens of times in a few seconds.evolve()now takes the night as a parameter (data.night(task.start), reusingDataProvider’s existing cache) instead of computing it fromTime.now()internally. Also a correctness fix: keying off “now” filed a task scheduled hours ahead under the wrong night’s bucket, which a laterFollowMerit/PerNightMeritcheck for that night would then miss. Seespecs/steering/scheduler-cpu-bound-merit-evaluation-stalls-event-loop.md.IERS auto-download being disabled (
iers_offline) now logs at info instead of warning – it’s a deliberate config choice, not an anomaly.
v2.0.0.dev64 (2026-08-05)
Fixed
ArchiveFile._uploadreferencingself._auth, which was never set (leftover from before auth moved toself._headers/token-based auth) – any actual write to apyobs-archivebackend crashed withAttributeErrorbefore the request was even sent.
v2.0.0.dev63 (2026-08-05)
Downgraded routine no-detection acquisition log lines (“no on-sky distance found”, a missed sun/star detection) from warning to info – neither is actionable on its own, since the acquisition loop just retries; warning level was noise for something expected to happen occasionally.
v2.0.0.dev62 (2026-08-05)
ImageFitsHeaderMixin’s missing-WCS-header warnings (CDELT1/2,CRPIX1/2, the CD-matrix) now fire once per module lifetime instead of once per frame, for cameras missing the relevant config/header fields.BackendTaskArchivenow follows pagination when fetching tasks/projects/observations frompyobs-robotic-backend(DRFPageNumberPagination, 100/page) via a newhttp_request_paginated()that follows the “next” link until exhausted – previously only the first page’s results were ever read, so tasks or projects past page 1 were silently invisible regardless of how long they’d existed.Mastermind’sget_next_observation/get_current_observationnow skip and log an observation whose task failed to resolve (Observation.taskleftNone– e.g. a deleted task, or the backend task-cache poll racing the observation-schedule poll) instead of returning it, which previously propagated a bareNoneintotask_runner.can_run()and crashed Mastermind’s background thread with anAttributeError.
v2.0.0.dev61 (2026-08-04)
Added CORS headers to
HttpFileCache.download_handler(previously blocked browserfetch()clients likepyobs-web-clientfrom cross-origin reads) and an opt-in token param, checked via constant-time compare, plus the requiredOPTIONSpreflight handler – replacingHttpFile’s Basic Auth, whichHttpFileCachewas silently ignoring since the server never enforced any auth at all. (#725)
v2.0.0.dev60 (2026-08-04)
Moved the
iers_offlinewarning log after handler setup, so it actually reaches a configured log handler instead of firing before one exists.
v2.0.0.dev59 (2026-08-04)
Added an
iers_offlineconfig option: the existingPYOBS_IERS_OFFLINEenv-var check is now also reachable viaApplication.__init__(iers_offline=...)and wired throughPyobsCLI.GLOBAL_CONFIG_KEYS, letting/etc/pyobs.yaml(or a--iers-offlineflag) disable astropy’s IERS/leap-second auto-download regardless of how the module process was spawned (pyobsdvs.pyobs-web-admin), unlike an env var, which only propagates through whichever launcher’s own environment it happens to inherit.Fixed
syslogbeing unsettable viapyobs.yaml: it was missing fromPyobsCLI.GLOBAL_CONFIG_KEYSeven though a--syslogflag exists, so apyobs:section in the config file silently had no effect on it.
v2.0.0.dev58 (2026-08-04)
pyobsd status’s CPU measurement is now opt-in (--cpu-interval SECONDS, sampling all modules over one shared sleep) instead of always-on – previously every “status” call paid a blocking 0.1s per running module, and that short a window is too close to/proc’s tick granularity to give a meaningful reading anyway. Uptime/RSS still report instantly.
v2.0.0.dev56 (2026-08-04)
Also prime the leap-second table when warming astropy’s IERS cache at startup (see dev55):
IERS_Auto.open()only covers UT1-UTC/polar motion, and the leap-second table is a separate auto-download that was still landing insidebasetelescope.py’s celestial-header task and blocking the loop there even after dev55’s fix. Usesupdate_leap_seconds()(not the lower-levelLeapSeconds.auto_open()), which both fetches it and installs it intoerfa.Exported
DummyModefrompyobs.modules.utils.
v2.0.0.dev55 (2026-08-04)
Astropy’s IERS cache is now primed once at startup, off the event loop via an executor, before module/comm setup – left implicit, the first UT1-UTC lookup happened inside
basetelescope.py’s periodic celestial-header task and triggered astropy’sauto_downloadsynchronously, blocking the event loop for however long the download took at an unpredictable point during normal operation.
v2.0.0.dev54 (2026-08-04)
XmppComm’s disco#info now tags each advertised<event>element with aroleattribute ("send","subscribe", or"send subscribe"), derived from splittingComm._registered_eventsinto_events_sent/_events_subscribed. Previously the two were merged into one undifferentiated set, so a client with no access to thepyobs.eventscatalog (e.g.pyobs-web-client) couldn’t tell which events a module actually publishes vs. which it only listens for.Comm.unregister_event()now also drops an event from_events_subscribedonce its last handler is removed, so a torn-down subscription stops being advertised (previously it stayed advertised forever). Seespecs/plans/event-role-advertising.md.
v2.0.0.dev53 (2026-08-03)
Offloaded
OnDemandScheduler’s per-timestep constraint/merit evaluation (find_next_best_task/check_for_better_task/can_postpone_task) onto a dedicated single-workerThreadPoolExecutor(newpyobs/robotic/scheduler/_executor.py’srun_cpu_bound), and cache target-independent sun/moon computations inDataProviderper timestep. These constraint/merit__call__methods are declaredasyncbut never actuallyawait– they run synchronous astropy/astroplan work directly on the event loop, blocking it for multiple seconds at real fleet scale; root cause of a 2026-07-30 “Event loop stalled” warning on the scheduler module. Same class of fix asVfs.write_image()/write_fits()below. Seespecs/plans/scheduler-event-loop-blocking.md. (#712)ImageProcessorgained anon_errorkwarg ("raise"(default) /"error"/"info"/"ignore") and ahandle_error(image, error)override point, andPipelineMixin.run_pipeline()(pyobs/mixins/pipeline.py) now catches each step’sImageErrorand dispatches per that step’s setting, instead of the whole chain aborting on the first exception from any step (previous behavior, equivalent toon_error="raise"everywhere). Non-ImageErrorexceptions still always propagate. Lets one step’s failure be non-fatal while another stays fatal within the same pipeline run – e.g. a calibration path that must have a valid WCS vs. a quick-look pipeline where a missing WCS is fine. Originally proposed in #328 as a nestedExceptionHandlerwrapper processor; rejected there for adding config nesting for what should be a per-step toggle.AstrometryDotNet’sexceptions: boolkwarg is now deprecated in favor ofon_error(exceptions=False==on_error="error";on_errortakes precedence if both are set) and its former internal try/except was removed –itson_error/handle_errordispatch now only happens when it runs as a pipeline step viarun_pipeline(). Calling it (or any other migrated processor) directly, outside a pipeline, always raises on failure regardless ofon_error/exceptions. Seespecs/plans/pipeline-step-error-control.md.Vfs.write_image()/write_fits()(pyobs/vfs/vfs.py) now serialize the FITS data (image.writeto()/hdulist.writeto(), including gzip compression for.fits.gztargets) viaasyncio.to_thread()instead of directly on the event loop. That serialization is CPU-bound and can take multiple seconds for larger frames, and running it inline blocked every other coroutine in the module for the duration – including state-publishing background tasks and comm handling – observed as multi-second event-loop stalls inpyobs-iagvt’sSunCamera/FTSright at thevfs.write_image()call. Callers that were working around this themselves (serializing viaasyncio.to_threadand uploading raw bytes withwrite_bytes()instead) no longer need to.
v2.0.0.dev52 (2026-07-29)
Guarded optional WCS/image-type handling against missing capabilities:
_ResponseImageWriter(astrometry.net response saving) unconditionally deletedPC/CDELTheader keywords after a solve, raisingKeyErrorwhen a header lacked one of them – now a no-op if the keyword is already absent.Acquisition/AutoFocusSeriesrequiredIImageTypefrom the configured camera via a hard proxy, unlikeAutoGuiding’s identical use case, which already tolerates a camera without it viasafe_proxy– now consistent.
v2.0.0.dev51 (2026-07-28)
No pyobs-core code changes – entirely root-causing and mitigating the ejabberd shaper/
xmpp_socket.erlreactivation bug behind the iag50srv capability-fetch timeouts (seespecs/plans/ejabberd-throughput-benchmarking.md); the shaper fix itself (raisingshaper.normal/shaper.fast) is an ejabberd-side configuration change, applied to iag50 production and baked into the newscripts/xmpp/install-ejabberd.shfor future hosts.
v2.0.0.dev50 (2026-07-28)
Fixed the XMPP wire serializer silently corrupting two more shapes of value:
Time-typed fields (e.g.WeatherState.time) had no encode/decode support at all and arrived client-side as a plain string; and a field declareddict[str, str]/strcould arrive as a strayboolif the isinstance-based encode dispatch won over the field’s own declared type (seen asModeState.modescontaining bools instead of strings). Follow-up fixes for a not-yet-restarted sender still running the old serializer:Timedecode no longer pins toisotformat (a legacy sender’s plainstr(Time)fallback used the space-separatedisoformat, which the strict parse rejected), and boolean/int/double values are now coerced tostron decode as well as encode.
v2.0.0.dev49 (2026-07-28)
Fixed
XmppComm.__init__raising a bare, message-lessValueErroron a malformed JID (found via a real login-window crash on a saved account with a trailing-slash, no-resource JID). The JID regex is now a module-level, properly end-anchored pattern (the previousre.matchwasn’t end-anchored, souser@domain/res/extramatched as valid), and the newis_valid_jid()lets a caller taking raw user input (e.g. a login window) validate upfront instead of finding out via an exception.Fixed
Application.run()crashing on a benign qasync shutdown race, where a Qt-backed event loop (pyobs-gui) can report itself stopped before the main task/cancellation gather has actually finished – now handled the same way as the existingCancelledErrorcase.Applicationcan now defer constructing itsModuleuntil the event loop is already running, via a newmodule_factory/loop_module_classalternative toconfig– letting the factoryawaitsomething like a login dialog’s submit signal before the module and its comm connection exist at all. Existing config-file callers are unaffected.
v2.0.0.dev48 (2026-07-27)
A module declaring a stateful interface but never publishing it (see the state-publishing enforcement below) now logs as an
error, not awarning– it’s a standing per-module defect that recurs on every startup, not a transient condition.
v2.0.0.dev47 (2026-07-27)
Every
Modulenow runs a background watchdog that detects the event loop itself stalling: it times its own wakeups against how long it asked to sleep for, so a synchronous blocking call anywhere in the module (or a background task) shows up as a logged stall – once when it starts, once when it clears, with total duration – instead of only being visible indirectly as peers timing out trying to reach that module.
v2.0.0.dev46 (2026-07-27)
The capability-fetch retry warning now includes the actual exception that caused the retry, instead of just noting that one occurred.
v2.0.0.dev45 (2026-07-27)
Fixed
FocusModel.open()skipping its state publish entirely when the weather module wasn’t reachable yet at startup (a common ordering race), which then always tripped the “declares state, but none published” warning; it now falls back to a placeholderfocus=0.0until the background update loop replaces it with a real value.
v2.0.0.dev44 (2026-07-27)
DummyCommnow takes a real name instead of a hardcoded"module"placeholder (Applicationfills it in from the config file’s stem for comm-less modules), fixing indistinguishablePYOBS_MODULElog tags and a guaranteed false positive against the config-stem-mismatch warning.Fixed
Comm’s log-forwarding failure handler re-queuing its own failure log through the same queue it was draining, turning a sustained send outage into an unbounded stream of self-generated failure reports.pyobsd’s per-module file logging (--log-file) is now opt-in (--file-log/--no-file-log, default off) instead of always passed to every started module regardless of the already-default-on journal logging; non-systemd deployments relying on the old always-on file logging need to pass--file-logexplicitly.Removed
IRunning.is_running()(and its ~12 implementers) now thatRunningState(pushed viacomm.set_state(IRunning, ...)) already covers the same information – both existed side by side for the same boolean.Extended the
max_agefreshness check (see below) toFollowMixin(default 3x its poll interval) andFocusModel’s temperature reads (default 2x), so a dead publisher’s frozen last value is treated as unavailable instead of trusted indefinitely.
v2.0.0.dev43 (2026-07-27)
Added an opt-in
max_agefreshness check toProxy.get_state()/wait_for_state(): a cached state value past itsmax_ageis now treated the same as not-yet-published (None/timeout) instead of trusted forever, even if the publisher’s update loop has died or a stanza was shaper-delayed by minutes.WeatherAwareMixinpassesweather_max_age(default 120s) so a deadWeatherupdate loop degrades to bad-weather within a couple of check cycles.Commnow tracks which interfaces have actually hadset_state()called, andModule.startup()warns when a module implements a stateful interface but never published it – caughtWeather/DummyCameraboth only ever publishingIWeather/ITemperaturesfrom a background loop, never synchronously fromopen(); both now also publish a placeholder value fromopen().Fixed non-TLS XMPP connections failing entirely (“No appropriate login method”) against ejabberd 26.4.0:
unencrypted_scramwas already set foruse_tls=False, but slixmpp also needsunencrypted_plainset explicitly before it will attempt PLAIN over a plaintext connection.
v2.0.0.dev42 (2026-07-26)
Removed racy, non-gating
has_proxy()startup checks inopen()(FlatField,BasePointing/Acquisition,AutoFocusSeries,FlatFieldScheduler) – one-shot, no-retry, 5s presence checks that routinely false-positived under any slow/delayed presence propagation without actually gating anything (every real use of the peer already re-resolves a fresh proxy and raises its own well-attributed error if genuinely missing).LocalCommnow broadcastsModuleOpenedEventonmark_ready(), matchingXmppComm’s presence-driven behavior – previously a module joining the local network after a peer had already started stayed invisible to that peer’sModuleOpenedEventhandlers until the peer restarted.BaseCamerano longer aborts closed-shutter (DARK/BIAS) exposures or sequences on bad weather, since the shutter being closed already means weather can’t affect them.Fixed the config-filename/module-name mismatch check (see below) firing a spurious warning against a legitimately-deactivated
_test.yaml-style config, by stripping the leading underscore from the config stem before comparing.
v2.0.0.dev41 (2026-07-26)
FlatField.open()now publishesIFiltersstate unconditionally, instead of only onceset_filter()had been called via RPC – previously the pubsub state node never got created on a fresh start, leaving subscribers (e.g. GUI widgets) retrying forever.The config-filename/module-name mismatch check added below now logs a warning instead of raising, so a genuine mismatch no longer prevents a module from starting at all.
v2.0.0.dev40 (2026-07-25)
A config file starting with an underscore (e.g. a deactivated
_test.yaml) is now logged with its leading underscores stripped from the module-name tag, instead of tagging log lines with the raw filename stem.
v2.0.0.dev39 (2026-07-24)
Applicationnow refuses to start a non-MultiModulewhose config filename doesn’t match its own comm-derived name, catching a class of drift where background tasks/RPC log lines use the real name but everything else falls back to the config file’s stem – silently splitting a module’sPYOBS_MODULElog tag in two whenever a config file is renamed independently of itscommconfiguration.
v2.0.0.dev38 (2026-07-21)
Suppressed astropy’s
VerifyWarningon FITS header comment truncation inImage.writeto(), the single choke point every FITS write funnels through – expected whenever a header value (aggregated from many modules) is long enough to leave no room for its comment within the 80-char card limit; the value itself is untouched, so the warning had nothing actionable to report.
v2.0.0.dev37 (2026-07-21)
Comm.get_interfaces()now waits briefly for a not-yet-discovered client instead of raisingKeyErrorinstantly, tolerating a caller’s own startup racing slightly ahead of this module’s peer discovery (which only populates once_got_onlinehas processed the peer’s presence). Also fixed a docstring/behavior mismatch: it promisedIndexErroron not-found, matching_get_client’s existing handling, but actually raisedKeyError.
v2.0.0.dev36 (2026-07-21)
Proxy.wait_for_state()now returnsNoneon timeout instead of lettingasyncio.wait_for’sTimeoutErrorpropagate – every caller (waitformotion.py,weatheraware.py,pyobs-iagvt’sSunCamera, others) already checked forNoneimmediately afterward, so a peer simply not having published yet was turning into an unhandled exception for all of them.
v2.0.0.dev35 (2026-07-21)
Event handler exceptions are now logged properly (with handler and event context) via
log.error()instead of surfacing only as asyncio’s generic “Task exception was never retrieved” warning at garbage-collection time, since_send_event_to_moduledispatches every handler as a fire-and-forget background task with nothing checking the result.
v2.0.0.dev34 (2026-07-21)
Added jitter to
_safe_send’s retry wait (send_event/_set_state’s underlying path), matching the indefinite-retry jitter added for capability/state-subscribe fetches below –without it, many modules’_safe_sendcalls retrying around the same moment (e.g. a fleet-wide restart) stayed in lockstep instead of spreading out.
v2.0.0.dev33 (2026-07-21)
Capability-fetch and state-subscribe retries (
_get_capabilities/_subscribe_with_retry) no longer give up permanently after a fixed attempt budget – they now retry indefinitely with capped exponential backoff and full jitter, stopping only when the peer actually goes offline or the last subscriber unsubscribes. A fixed budget could be exhausted across many module pairs at once by a simultaneous fleet-wide restart, leaving capabilities/state permanently missing until a manual restart.
v2.0.0.dev32 (2026-07-21)
XmppComm’s XEP-0199 keepalive ping interval/timeout is now configurable, instead of a fixed 30s default that can be shorter than an ejabberd shaper’s IQ reply delay under load – letting a deployment tolerate a known-slow server instead of only a genuinely dead connection.
v2.0.0.dev31 (2026-07-21)
Fixed pubsub subscribe retries only catching
IqError, not theIqTimeoutthat_safe_sendalso raises once its own internal retries are exhausted – an uncaughtIqTimeoutkilled the whole 30-attempt subscription retry loop after a single slow response, surfacing only as an unretrieved background-task exception instead of the intended “could not subscribe” warning, with the subscription then permanently abandoned.
v2.0.0.dev30 (2026-07-21)
WeatherAwareMixinno longer parks on the first weather-check connection hiccup – a common right-after-restart race where XMPP roster/presence for the weather module hasn’t settled yet. A connection failure now only counts as bad weather after 3 consecutive failed checks.
v2.0.0.dev29 (2026-07-18)
Modules no longer accept RPC calls until they’ve actually finished starting up. Some drivers (e.g. camera modules connecting to hardware) take a while inside
open(), but were previously reachable over XMPP – and visible to peer discovery – the instant they connected to the server, long beforeopen()returned. NewModuleState.STARTING, set at construction and cleared by the newModule.startup()once the fullopen()override chain (base setup plus every subclass’s own) has completed;Module.execute()now rejects any call outside a small introspection/recovery whitelist (get_permitted_methods,reset_error) with the newModuleStartingErrorwhile a module is stillSTARTING.XmppComm/XmppClientalso hold back the initial XMPP presence broadcast until a module reachesREADY, closing a race where a peer reacting to_got_onlinecould read capabilities (e.g. hardware-dependent ones likeIWindow/IBinning) that hadn’t been published yet – scoped to comms with an actual startingModuleattached, so bare/GUI-styleXmppCommusage announces itself exactly as before.ApplicationandMultiModuleboth callModule.startup()instead ofopen()directly now; any other code that opens a module standalone (a custom script, a test) needs to do the same, or callset_state(ModuleState.READY)itself afteropen()– seemodule-startup-gating.
v2.0.0.dev28 (2026-07-18)
New
InvalidArgumentErrorfor bad-argument validation on RPC-exposed methods (unknown filter name, invalid focus value, invalid config parameter, out-of-rangegrab_sequencecount/delay, …) – previously these stayed as plainValueError, which works fine locally (LocalComm, direct calls, tests) but silently degrades toUnclassifiedErrorthe moment the same call crosses XMPP, sinceValueErroris a builtin and never in the registry. That inconsistency is exactly the kind of bug that only shows up once code moves from a local test to a networked deployment. Fixed forIConfig.get_config_value/get_config_value_options/set_config_value,IDataSequence.grab_sequence,ITrackingMode.set_tracking_mode,IFocuser.set_focus,IFilters.set_filter,IMode.set_mode, andIWeather.get_sensor_value(MockWeather’s implementation – the realWeatherclass’s ownValueErrorthere is about a malformed station response, not a bad argument; see the next entry). Also reused the existingDeviceBusyErrorforFlatField.flat_field/FlatFieldScheduler.run’s “already running” check, which was never actually about a bad argument either. Scoped out first (seeDESIGN_exception_handling.md) before touching anything: real driver repos (e.g.pyobs-sbig) likely have the identicalValueErrorpattern for real hardware and would need the same companion fix, not reachable from this PR.New
WeatherResponseError(pyobs.modules.weather.weather) forWeather.get_sensor_valuegetting back a station response missing itstime/valuefields – a different shape of problem from the bad-argument case above (an external dependency being flaky, not a caller mistake), same reasoning asBodyResolutionError: plausibly transient, worth retrying, not the caller’s fault.WeatherStatus.status’s similar-lookingValueError(weather_state.py) turned out to be unreachable from any RPC caller at all – it only fires inside a background polling loop that already catches it broadly and just logs a warning – so it’s left untouched.Fixed
UnclassifiedError.original_typesilently not surviving the wire:Module.execute()wraps a non-domain exception (IndexError, a vendor SDK exception, …) asUnclassifiedErrorbeforerpc.pyever sees it, butfault_to_xmlwas serializing the wrapper’s own class name instead of the original type it was tagged with – sinceUnclassifiedErroritself is a registered type, the caller reconstructed a fresh one withoriginal_typenever set, making a remoteIndexErrorand a remoteValueErrorindistinguishable. Now serializes the original type name instead, so the caller’s own registry lookup runs against it and correctly repopulatesoriginal_typeon the (still-unresolvable)UnclassifiedErrorit falls back to. Also fixed in the same pass: every exception crossing the wire had its message doubled ("<ClassName> <ClassName> message") once displayed, because the message field serializedstr(exception)(already"<ClassName> message") instead of the raw message, which reconstruction then fed back in as the new instance’s own message before formatting it again.Docstring sweep across every interface flagged by the exception-handling audit (16 of 27 documented interfaces had at least one mismatch) –
Raises:clauses now match what’s actually raised:IFocuser/IPointingRaDec/IPointingAltAz/IPointingBody/IPointingOrbitalElements/the Heliocentric*/Helioprojective family gainNotSupportedError/MissingObserverError/AltitudeLimitError/BodyResolutionError/InvalidOrbitalElementsError(or a note that they propagate from the underlying RA/Dec move);IAutoFocus/IAcquisitionnow document the types their own@raisesalready declared instead of a staleValueError;IData/IDataSequencedocumentDeviceBusyError;IFocusModeldocuments its three new leaf types;IExposureTime/IFlatField/IWeathergain the clauses their implementations already needed.FlatField.set_filter’s docstring copy-paste bug (“If binning could not be set” on a filter setter) is fixed. The handful of interfaces with zero concrete implementers anywhere in this repo (ICalibrate,ISyncTarget,IMultiFiber.set_fiber,IPointingSeries,IRotation,IScriptRunner.run_script) gain a plausibleRaises:clause so a future implementer has a contract to follow instead of guessing – the same guess that produced several of the mismatches this sweep fixes. Documentation only, no behavior changes. Note: several interface methods still document plainValueErrorfor bad-argument validation (as opposed to domain operation failures) – deliberately not promoted to typed exceptions in this sweep; seeDESIGN_exception_handling.md’s “Still open” section for the tradeoff.Documented the
AbortedErrorcontract on everyabort_event-taking hook inpyobs-core(BaseCamera/BaseSpectrograph._expose(),BaseTelescope._move_radec/_move_altaz) – nothing had ever told driver authors which type to use for “this was cancelled, not a real failure,” so two of pyobs-core’s own in-tree implementations (DummyCamera,DummySpectrograph) had each independently guessed a different wrong type (InterruptedError,ValueError) for the exact same condition. Both now raiseexc.AbortedError, as does_DummyTelescopeBase.set_focus’s equivalent abort check. (Two more instances of the same guessed-wrong-type pattern, inpyobs-sbig/pyobs-fli, are a companion fix in those repos, not something this PR can reach.)Documented the domain/transport split in
pyobs/utils/exceptions.pyas a deliberate axis:RemoteErrorand its subtree (RemoteTimeoutError,ForbiddenError) mean “the call itself didn’t reach/return,” which doesn’t benefit from the same fine-grained-per-reason treatment domain exceptions get – documentation only, no behavior change.First sweep of concrete exception-typing gaps (goal 5: specific types over generic ones/bare builtins). New cross-cutting types in
pyobs.utils.exceptions:DeviceBusyError(“this device can’t service this request right now, back off and retry”) andNotSupportedError(“this module doesn’t implement this optional capability at all”).CameraException(BaseCamera) andAcquireLockFailed(LockWithAbort, a plainExceptionthat leaked out ofmove_radec/move_altaz/set_focus/stop_motion/roofinit/parkunconverted) are retired – both meant the same thing, “device busy,” now unified asDeviceBusyError. The telescope/roofinit()/park()boundary specifically translates a lock-acquisition failure (or any other failure) intoInitError/ParkErrorinstead, perIMotion’s own documented contract, followingBaseCamera.__expose()’s existing catch-and-translate pattern. Capability-checkNotImplementedErrorsites (an alt/az-only telescope’smove_radec,ScienceFrameAutoGuiding.set_exposure_time, a dummy telescope’sset_focus_offset) now raiseNotSupportedErrorinstead.BaseTelescopegainedMissingObserverError/AltitudeLimitError/BodyResolutionError/InvalidOrbitalElementsError(allMotionError) for its previously-bareValueErrorsites.FocusModelgainedWeatherDataError/FocusTimeoutError/MissingSensorError(allFocusError). NewScriptError(pyobs.robotic.scripts) wraps whatever a script’srun()raises that isn’t already a domain exception, following the same pattern;AutoFocusScript.can_run()now checks for a target itself instead of only discovering its absence afterrun()has already started.BaseVideo/BaseSpectrograph.grab_data()’s “no image”ValueErrorsites now raiseGrabImageError, matchingBaseCamera. All new leaf types live next to the code that raises them (basetelescope.py,focusmodel.py,pyobs.robotic.scripts), not bolted ontoexceptions.py, now that the registry from the previous step lets a domain exception survive the wire regardless of which module defines it. Fifth step of the exception-handling rollout inDESIGN_exception_handling.md(tracks #446); remaining items in that sweep (ScriptRunner’s per-script leaves if ever wanted, driver-repoAbortedError/NotImplementedErrorfixes) are out of scope for apyobs-corePR alone.RPC calls over XMPP now carry a correlation id end to end: the origin-side log line for a domain exception (
Module.execute()’s catch block) includes(call_id=...), and the same id is attached to the exception the caller receives asexception.call_id– reusing XEP-0009’s existing per-calliq["id"]rather than adding new plumbing. Lets an operator debugging a caller-sideFocusErrorjump straight to the matching detailed log on the module that actually raised it, instead of neither side’s log line pointing at the other. Purely additive, no migration required; not set forLocalComm/MultiModulecalls, which are already in the same log stream as the caller. Fourth step of the exception-handling rollout inDESIGN_exception_handling.md(tracks #446).Constructing a
PyobsErroris now side-effect-free, ordinary Python.raise exc.FocusError(...)always raises aFocusError– it no longer risks silently coming back as aSevereErrorinstead, which could happen because the old severity-escalation metaclass intercepted construction, not raising or catching.SevereErroris retired entirely: nothing in this repo or any sibling project ever caught it specifically, its only real consumer wasregister_exception’scallback(already used everywhere; production code never actually setthrow=True), which already does the meaningful part itself (set_state(ModuleState.ERROR)).register_exception/handle_exceptionmove from module-level free functions with process-global state toModule._register_exception()/an internal_record_exception(), called fromModule.execute()’s catch block (the same chokepoint that already classifies and logs) – fixing a real cross-instance bug as a byproduct, where twoModuleinstances in the same process (e.g. underMultiModule, or two instances watching the same remote module) used to share one counter. Ten in-tree call sites plus one inpyobs-alpacaneed the mechanicalexc.register_exception(...)->self._register_exception(...)rename (thethrowparameter is gone with the substitution it existed for). Also: any non-PyobsErrorexception escaping a module’s method body is now wrapped asUnclassifiedErrorright inexecute(), not only on the XMPP fault path, soLocalComm/MultiModuleget the same safety net as XMPP;RPC._on_jabber_rpc_method_callno longer logs domain exceptions itself sinceexecute()already did (it still logs failures that never reachexecute(), like malformed RPC parameters). Third step of the exception-handling rollout inDESIGN_exception_handling.md(tracks #446).A remote domain exception now arrives at the caller as its real type, catchable directly (e.g.
except exc.FocusError:around a proxy call actually fires now) – previously every remote failure, transport or domain, arrived wrapped inInvocationError(now retired entirely), so a caller could only catch the broad wrapper and manually unwrap.exception. Exception classes are now resolved via a registry (PyobsError.resolve(), populated automatically via__init_subclass__) instead of agetattrlookup restricted topyobs.utils.exceptions, so a domain exception can now live anywhere (a driver package, apyobs-coresubmodule) and still survive the wire, keyed by fully-qualified name rather than bare class name. An exception that can’t be resolved (a raw builtin, a vendor SDK exception, or a domain type whose defining module was never imported in this process) arrives as the newUnclassifiedErrorinstead of silently degrading to a genericRemoteErrorwith only the message surviving – the original type’s qualified name is preserved asUnclassifiedError.original_type.UnclassifiedErrorjoinsModuleError/SevereErroras unsuppressible and always-loud.PyObsErroris renamed toPyobsError(naming consistency withPyobsArchive,PyobsCLI, etc.) and its constructor is nowPyobsError(message=None, **context), storing every keyword generically as an attribute –RemoteError/RemoteTimeoutError/ForbiddenErrorno longer have their own constructors, so direct construction now takesmodule=/sender=/method=as keywords instead of fixed positional arguments. Breaking change for any external code constructing these directly, catchingexc.PyObsError/exc.InvocationErrorby name, or relying on a remote failure always arriving as someRemoteErrorsubclass (pyobs-gui’sbase.py/mainwindow.pyreferencePyObsErrorby name and need the rename applied). Second step of the exception-handling rollout inDESIGN_exception_handling.md(tracks #446).Every RPC-exposed method raising a domain
PyobsErrornow logs a quiet INFO line locally by default, without a traceback – previously this only happened for methods explicitly decorated with@raises(...)(used on exactly two methods), and every other domain exception logged at ERROR with a full traceback despite the caller already receiving the same error.@raisesno longer controls log level (documentation value only, for now);Modulegained_disable_exception_logging(*exception_types)for a module to opt a high-frequency exception type out of even the quiet line entirely, since the caller already has it.ModuleError/SevereErrorare exempt from both the quiet default and the opt-out – they always log loudly, since both mean “this needs a human’s attention at the source,” not “an anticipated domain failure.” Part of the first step of the exception-handling rollout inDESIGN_exception_handling.md(tracks #446).
v2.0.0.dev27 (2026-07-16)
Fixed
PhotometryFocusSeriessystematically underreporting its focus error by a factor of its own square root:fit_hyperbolareturns(focus, variance), not(focus, error).curve_fit(pyobs.utils.curvefit.fit_hyperbola) now passesabsolute_sigma=True, so the reported variance reflects the actual per-point measurement uncertainty passed in viay_errinstead of being silently rescaled to force a reduced chi-square of 1; the weighted-mean focus calculation now uses inverse variances accordingly.Guiding-statistics FITS keys (
GUIDING UPTIME/RMS/RMS1/RMS2) now get an explicitHIERARCHprefix, avoiding astropy’sVerifyWarningfrom auto-promoting them (they exceed the 8-char keyword limit).
v2.0.0.dev26 (2026-07-16)
ICamera/ISpectrographno longer inheritIExposure– they’re now pureIDataidentity interfaces (“this module produces images/spectra”), not “…and has an exposure clock.”BaseCameraandBaseSpectrograph(which push realExposureState) now declareIExposureexplicitly instead of getting it for free;PipelineCameradropsIExposureentirely instead of publishing a fabricated, never-updatedExposureStatefor a pipeline run that has no exposure to report progress on. Breaking change for any external module that subclassesICamera/ISpectrographdirectly (not viaBaseCamera/BaseSpectrograph) and relied on inheritingIExposureimplicitly – it needs to addIExposureto its own bases now if it actually wants to publish exposure state (e.g.pyobs_iagvt’sSunCamera, tracked separately, not fixed here).pyobs-gui’sCameraWidgetneeds no change: its exposure-progress panel was already conditional onhas_proxy(..., IExposure). SeeDESIGN_ICamera_IExposure.md.Added
Image.trim(), unifying three previously-independent implementations of TRIMSEC parsing (inline inProjectedOffsets,fitssec(), andPipeline.trim_ccddata()) into one method that also keepsmask/uncertaintyaligned withdata. ShiftsCRPIX1/CRPIX2to account for the new origin – a correctness fix, since none of the three prior implementations did this, silently leaving a stale WCS reference pixel after trimming. RaisesValueErrorif a catalog is already attached, since its pixel coordinates would otherwise silently go stale against the trimmed frame – run source detection after trimming, not before. RemovedPipeline.trim_ccddata(); its two call sites now trim theImagebefore converting toCCDDatainstead of after – breaking change for any external code calling it directly.fitssec()’s parser is now shared (pyobs.utils.fits.parse_section_bounds) and raises a well-definedValueErrorfor a malformed section keyword instead of an arbitraryIndexError/ValueError. SeeDESIGN_Image_trim.md.Added an optional
delayparameter (seconds between grabs, default0) toIDataSequence.grab_sequence(), for cadence control between grabs without resorting to dithering/offsets (a pointing-layer concern out of scope for this interface). Bothabort_sequence()andabort()cut a pending delay short instead of idling out the full wait once the sequence has already been told to stop.Fixed
Time.night_obs()crashing during polar day/night: it assumedobserver.sun_set_time()always returns a validTime, but astroplan returns a masked value when the sun never crosses the horizon in the search window; now falls back to the observer’s local calendar date.Fixed
Schedulerreading the private_observerattribute instead of the publicobserverproperty in its altitude/airmass logging, which broke silently (AttributeErrorswallowed by the worker loop) on mocked scheduler instances in tests, aborting scheduling beforeadd_observationswas even called.Enriched existing scheduler/telescope log lines (per-block schedule summary, altitude-limit error) with the computed altitude/azimuth/airmass on each side, so a mismatch between what the scheduler validated and what the telescope sees at move time can be diagnosed without new per-candidate logging on hot paths.
Fixed a missing
OBJECTFITS header:Task/LcoTask/LcoScript.get_fits_headers()re-validated a brand-newScriptinstance from the stored config instead of reusing the instancerun()actually executed, losing any state (like the target name) accumulated during the run; the actively-running script instance is now cached and reused for header lookups.Fixed
CameraSettingsMixinstill calling the synchronous, cache-onlyget_capabilities()after capability fetches became asynchronous background tasks (previous release), which made “Could not get full frame size.” fail deterministically for everyIWindowcamera; now awaitsProxy.wait_for_capabilities()for aProxycamera.Fixed
CircularMaskdestroying pixel data by zeroing it directly instead of flagging viaimage.mask– it now sets (OR’s)image.maskand leaves the underlying data untouched.
v2.0.0.dev24 (2026-07-15)
Comm’s proxy capability fetch (previously a single blocking disco#info query with no retry) now runs as a background task retried up to 3 times, mirroring the existing state-pubsub race fix, plus a newProxy.wait_for_capabilities()for callers that need a guaranteed value; a peer that hadn’t finished starting up no longer produces a permanent missing-capability warning.
v2.0.0.dev23 (2026-07-15)
Added
IDataSequence(grab_sequence()/abort_sequence(), pushedDataSequenceState) for taking a counted sequence of grabs in one call instead of driving a client-side loop ofgrab_data()calls. Implemented byBaseCamera.grab_sequence()returns immediately and runs the sequence in the background;abort_sequence()stops after the current grab finishes, while the existingabort()now also cancels the rest of a running sequence. SeeDESIGN_IDataSequence.md.Fixed a
FocusModelstartup race fetching module temperatures: it usedget_state(), which only reflects whatever pub/sub state has arrived so far, crashing with a confusing “sensor not in data” error if the target module hadn’t publishedITemperaturesyet; now useswait_for_state(), matchingwaitformotion.py/weatheraware.py.BaseCameranow aborts the current exposure/sequence onBadWeatherEvent(hard-stopping viaabort(), clearing any runninggrab_sequence()count), matching the existingFlatField._abort_weatherpattern – previously this only worked client-side, viapyobs-gui’s own sequence loop. (#547)Published previously-missing states for the dummy telescope/spectrograph modules:
_DummyTelescopeBase(and itsDummyAltAzTelescope/DummyRaDecTelescopesubclasses) never publishedIPointingAltAz;DummySolarTelescopenever published its Heliocentric/Heliographic/Helioprojective pointing states until a correspondingmove_*call had already been made;BaseSpectrograph/DummySpectrographnever publishedIExposure. All three now publish atopen()(and update live), mirroringBaseCamera.Published previously-missing initial/
IReadystates for several modules that formally require them via interface inheritance but never published them, leaving subscribers to retry indefinitely:AutoGuiding/ScienceFrameAutoGuiding’sIExposureTime,PipelineCamera’s state entirely (including theIExposurerequired byICamera),FlatField’sIMotion, andMotionStatusMixin’s separately-enumeratedIReady(whichIMotionimplies) – foldingBaseRoof’s duplicate ready-derivation andDummyMode’s hardcoded publish into the mixin’s shared default.Added
Comm.unregister_event(), the missing inverse ofregister_event()– previously a caller torn down while the app keeps running (e.g. a GUI widget discarded on client disconnect) could never stop receiving events, keeping it alive and firing on every future matching event. (#438)Fixed a
ZeroDivisionErrorinFlatFielder._calc_new_exptimewhen a test flat-field’s median counts equal or fall below the bias level (e.g. sky still very dark); now clamps to the max exposure-time increase factor instead. (#481)
v2.0.0.dev21 (2026-07-15)
Bounded
XmppComm._safe_sendwith its own 15sasyncio.wait_for()timeout, instead of relying entirely on slixmpp’s internal IQ timeout (default 120s, which could fail to fire even on a healthy connection) – an unbounded hang here could freeze the calling module, including insideopen(), which blocks the whole event loop. (#664)
v2.0.0.dev20 (2026-07-15)
Internal only: fixed the remaining deprecated
self._xmpp[...]-style slixmpp plugin-subscript access inXmppComm, following up on the earlier migration toclient.plugin[...].
v2.0.0.dev19 (2026-07-15)
Reintroduced the old
IPointingHGSlon/lat contract asIPointingHeliographicStonyhurst, now a separate interface fromIPointingHeliocentricPolarinstead of a repurposing of it – drivers needing Heliographic Stonyhurst tracking (e.g.pyobs_iagvt’sSolarTelescope) should implement this one instead.Removed
pyobs.modules.utils.AutonomousWarning(played warning sounds while anIAutonomousmodule was running). Found while writing tests for it:started_sound/stopped_soundwere stored but never read anywhere, and_check_autonomous()’s sound selection looked inverted (it logged “Robotic systems started” but playedstop_sound, apparently copy-pasted from_check_trigger()’s toggle logic without adjusting the polarity) – breaking change for anyone using it, no replacement provided.Fixed three
BaseVideobugs found while reviewing it: a request-list race wheregrab_data()removed its request from_image_requestswithout holding_image_request_lock, while_set_image()iterates/mutates the same list under that lock across anawait(could skip another pending request); a fallback-filename bug where_finish_image()’s"image.fits"fallback (used when no filename pattern is configured) never reachedimage.header["FNAME"], the actual cache key, raisingKeyErrorinstead of degrading gracefully; and removed the dead_new_image_event(set every frame, never waited on anywhere – the real new-image notification is the separateNewImageEventcomm event).Split
DummyTelescopeintoDummyRaDecTelescope(+IOffsetsRaDec),DummyAltAzTelescope(+IOffsetsAltAz), andDummySolarTelescope(+IPointingHeliocentricPolar,IPointingHeliographicStonyhurst,IPointingHelioprojective– always tracks the Sun via a dedicated background task, no compatibility alias for the old class name). Seedummy-telescope-split-design.md.Renamed
IPointingHGStoIPointingHeliocentricPolarand its fields fromlon/lattomu/psi, matching the existingHeliocentricPolarTarget– the old fields actually represented Heliographic Stonyhurst coordinates, a different frame; breaking change for any external driver implementing it (e.g.pyobs_iagvt, tracked separately).Object.locationis now derived fromObject.observerinstead of being stored and propagated to child objects independently, removing a source of location/observer divergence. Thelocationconstructor argument is unchanged, but only affects the defaultobserverbuilt from it.Fixed
Scheduler._on_task_finishedsilently ignoringTaskFailedEvent(registered as its handler alongsideTaskFinishedEvent, but its own guard only accepted the latter – a sibling class, not a subclass), so a failed task never cleared_current_task_idor triggered a re-schedule even withtrigger_on_task_finishedenabled.Implemented non-sidereal tracking: new
ITrackingMode/ITrackingRateinterfaces (discrete vs. continuous tracking modes) andIPointingBody/IPointingOrbitalElements(track a named body or a set of orbital elements), plusARCSEC_PER_SEC/AUunits.BaseTelescopeimplements the continuous-tracking background task, propagating a tracked body or orbital elements (two-body Kepler solve, hand-rolled rather than viapoliastro– ~6us/call for the propagation itself, dominated by astropy’s own frame-transform cost) into a live RA/Dec correction, with the refresh interval clamped against the mount’s own publishedmin_update_interval(via the newComm.get_own_capabilities(), mirroringget_own_state).DummyTelescope(see split above) is the reference implementation;BaseTelescopealso now publishesAltAzStateforIPointingAltAztelescopes.track_body()resolves viaastropy.coordinates.get_body/JPL Horizons only – deliberately no automatic MPC/NEOCP-by-designation lookup, since MPC has no formal REST API and scraping would be riskiest for exactly the fresh-NEOCP-object case the feature exists for; a caller with elements in hand usestrack_orbital_elements()directly. Seetracking-mode-design.md.Fixed two crash paths in
FitsHeaderMixin’s header-building:_fitsheadermixin_add_framenum()readhdr["DAY-OBS"]unconditionally, but that key is only set whenDATE-OBSwas present, crashing withKeyErrorinstead of the warn-and-skip its own log message implied; and_fitsheadermixin_add_fits_headers()calleddate_obs.night_obs()(which needs a realObserver) whenevernight_obs=True(the default) even with no observer configured, crashing withAttributeErrorinstead of falling back to the plain calendar day like the location handling right above it already does.Fixed
FlatFielder’s deviation check comparing a fraction againsttarget_count(a raw count-rate) instead of the already-stored_allowed_offset_frac, so the “retry if the flat is way off target” check could essentially never trigger.Added an import-time registry for
Interfacesubclasses (pyobs.interfaces.interface.get_registered_interface/registered_interfaces), and switchedModule._get_interfaces_and_methods,XmppComm._get_interfaces, andComm._interface_names_to_classesto resolve through it instead of hardcoded lookups against thepyobs.interfacesmodule namespace – previously a module implementing an externally-defined interface (e.g. a driver package’s own) was never discovered, advertised, or resolved by name at all.Fixed XMPP wire serialization corrupting/mishandling numpy scalar types:
numpy>=2.0changednp.float64’srepr()to"np.float64(x)", corrupting the<double>element text and aborting reconstruction of the whole enclosing dataclass (e.g. droppingIModulecapabilities.versionalongside a malformedlocation.longitude);np.integertypes aren’t subclasses ofintat all and silently fell through to the<string>fallback instead of<int>. Anynp.genericscalar is now converted to its native Python type before serialization.
v2.0.0.dev18 (2026-07-12)
Fixed
DummyTelescope.set_focus_offsetsilently logging an error instead of raising, which made remote callers see a false success; its M1/M2 temperatures now drift likeDummyCamera’s sensors instead of staying static after startup.pyobsdnow defaults to sending module logs to the systemd journal (--syslogis on by default; pass--no-syslogto disable it).Added
pyobsd logs [module] [journalctl arguments...], a thin passthrough tojournalctlfiltered to the module’s journal fields.
v2.0.0.dev17 (2026-07-11)
Added a
/pinghealth-check endpoint toHttpFileCacheandBaseVideo, for verifying HTTP connectivity without touching the file/image cache.
v2.0.0.dev16 (2026-07-10)
Added
IStructuredConfig, letting a module push/apply its whole config dataclass as a unit (schema auto-derived viadataclass_to_schema), complementingIConfig’s per-field get/set.Added
pydantic_to_schema, the Pydantic-model counterpart todataclass_to_schema, for module configs (e.g. pyobs-iagvt’sFTSConfig) that need Pydantic’s own validation.Renamed
HeliocentricPolartoHeliocentricPolarTargetfor naming consistency.Replaced
HelioprojectiveRadialTargetwithHelioprojectiveTarget, using the Helioprojective frame’s Tx/Ty (arcsec) directly instead of a radial (psi/delta) representation.
v2.0.0.dev15 (2026-07-10)
Fixed
HttpFileCacherejecting uploads with “413 Request Entity Too Large” becauseclient_max_sizewas never passed through to configure the upload limit.
v2.0.0.dev14 (2026-07-09)
Fixed
Scheduler,Trigger,Kiosk,PointingSeries,Weather,MockWeather, andMastermindnever publishing their advertisedIRunningstate, leaving subscribers retrying indefinitely.The LCO schedule backend now logs the next observation after a schedule update, matching
BackendObservationArchive.
v2.0.0.dev13 (2026-07-09)
Internal fixes only: resolved the remaining pyrefly type-check errors blocking CI.
v2.0.0.dev12 (2026-07-09)
Added
HelioprojectiveRadialTargetfor solar coordinate scheduling.Added
MockWeather, a deterministic in-memoryIWeatherimplementation for tests and simulations.IAcquisition/IAutoGuidingnow publish live state (AcquisitionState/AcquisitionAttempt,GuidingState); fleshed out their dummy modules to match.AcquisitionResult/AcquisitionAttemptnow use a singleoffset_frame(RA_DEC/ALT_AZ) plusoffset_lon/offset_latinstead of four separateoff_ra/off_dec/off_alt/off_azfields, and attempts track the 2D offset per iteration.ApplyOffsets(and its RA/Dec and Alt/Az subclasses) now return anOffsetResultwith the actually-applied correction instead of a barebool.IMode.set_modenow takes a group name instead of a positional index.Added
IRunningsupport toIAutoFocus/DummyAutoFocus.Implemented script dispatch for the
SCRIPTconfig type (LcoScript), selecting a nested script viaextra_params.script_name.Fixed context (
comm/observer/vfs) not being propagated toPortal,LcoScheduleReader/LcoScheduleWriter, andLcoTaskwhen constructed directly, which broke proxy lookups.Fixed a missing
requestfield causing LCO script validation to fail.Fixed
BaseVideonever publishingIImageTypestate (unlikeBaseCamera).Made the
telegramimport lazy in theTelegrammodule, so it’s no longer a hard dependency for unrelated modules inpyobs.modules.utils.Added a “What’s New in pyobs 2.0” docs page tracking user-facing changes for the 2.0 release.
v2.0.0.dev11 (2026-07-05)
Implemented access control (ACLs) for module RPC calls, via an
acl:config block next tocomm:(allow/denypolicy,enforce/logmode,IModule.get_permitted_methods()).Completed the
Unitannotation rollout across all applicable interface signatures.Interface and event schemas, including versioning, are now fully published via service discovery (disco#info), enabling a mixed-version-fleet diagnostic.
IAcquisition.acquire_target()now returns a typedAcquisitionResultinstead ofdict[str, Any].Module’s constructor no longer takes anameparameter; a module’s name always tracks itscommobject’s own identity (XMPP JID /LocalCommname) instead.Added
--syslogtopyobsd, forwarding it to thepyobsprocesses it launches.A module now shuts down gracefully instead of endlessly reconnecting when kicked from XMPP due to a JID conflict.
Fixed an XMPP reconnect storm after an ejabberd outage, and a module reconnect that could be silently dropped by a stale presence callback.
Fixed
DummyTelescope.park()not stopping an in-progress slew.Fixed a crash when a cooling setpoint or config name is
None.Fixed CRITICAL log lines being journaled with the wrong priority under
--syslog.Hardened RPC parameter parsing (
xml_to_params) against malformed input.Fixed phantom XMPP state subscriptions for composite interfaces.
A global
pyobs.yamlconfig file (including under/opt/pyobs/storage/) is now looked up in addition to a module’s own config file.
v2.0.0.dev10 (2026-07-02)
Interface features are now version-tagged in service discovery, giving a diagnostic for mixed-version fleets.
IAutoFocus.auto_focus()now returnsAutoFocusResultinstead of a tuple; added liveAutoFocusState; removed the oldauto_focus_status().Added
OptimalFocusStatefor structured focus-model state tracking.IWeathermigrated to structured live state; droppedstationfrom sensor readings.IFitsHeaderBefore/IFitsHeaderAfternow returndict[str, FitsHeaderEntry]instead ofdict[str, tuple[Any, str]].Removed the old XML-RPC cast pipeline (
pyobs.utils.types).
v2.0.0.dev9 (2026-07-01)
Linked all
pyobs-*module docs, now hosted on docs.pyobs.org.
v2.0.0.dev8 (2026-07-01)
Documented optional extras and CLI tools in the README.
v2.0.0.dev7 (2026-07-01)
Minor internal fixes (Dependabot configuration, import scoping in
DummyCamera).
v2.0.0.dev6 (2026-06-30)
Renamed the
state/capabilitiesproxy accessor methods toget_state/get_capabilitiesfor clarity; both are now also available directly onInterface.Added IERS offline mode support via the
PYOBS_IERS_OFFLINEenvironment variable.The
pyobsservice now loads environment variables from/etc/default/pyobs.Added
astropy-iers-dataas a direct dependency.
v2.0.0.dev5 (2026-06-30)
Refactored
capabilitieshandling for consistency across interfaces.
v2.0.0.dev4 (2026-06-29)
Interface
Stateclasses are now module-level dataclasses rather than nested classes.
v2.0.0.dev3 (2026-06-29)
Removed
pyobs.utils.simulation(SimWorld/SimTelescope/SimCamera) with no replacement.Added
DummyVideofor simulated video streaming.IModenow usescapabilities/statedataclasses instead of separate mode-group methods.Added an optional
senderattribute toLogEvent.
v2.0.0.dev2 (2026-06-29)
Rolled out live state to (almost) all state-bearing interfaces; removed the corresponding
get_*/is_*RPC methods project-wide in favor of subscribing to state.Added a shared XML serializer for both RPC and state payloads (
pyobs.comm.xmpp.serializer); rewrote the XMPP RPC layer on top of it.Added
Proxy.wait_for_state(returns cached state immediately, or waits for the first update).Added
capabilities— fixed-for-lifetime values published via service discovery, alongside the newstatemechanism.A module’s online/ready/error status is now tracked via XMPP presence rather than RPC; removed
IModule.get_state()/get_error_string().LocalCommgained state, capabilities, and presence support to matchXmppComm.Removed
ILatLonand the never-implementedDbusCommbackend.
v2.0.0.dev1 (2026-06-22)
Added
version(default1) toInterfaceandEvent.Proxyis now obtained viaasync with self.proxy(...) as x:only; the long-livedawait self.proxy(...)pattern and thecache_proxiesoption are removed. Addedhas_proxy/safe_proxy.Added
Unitannotations for physical quantities on interface signatures.Added the first live state implementation over XMPP PubSub, piloted on
ICooling.
[1.53.0] - 2026-06-19
Replaced the threading-based XMPP client internals with asyncio throughout.
Added
--syslogto log to the systemd journal, for bothpyobsandpyobsd.pyobsdnow names each module’s log file after its config file (e.g.camera.yaml→camera.log) instead of a fixed name, and takes its default module name from the config file.Added
ObservationState.WINDOW_EXPIRED.Added
HelioprojectiveRadialTarget.The scheduler now guarantees a minimum scheduling window length and excludes the currently-running task from “is there a better task” checks.
Made XMPP message sending more robust: queues messages, skips repeated ones, and checks whether the user is actually logged in.
Fixed object-ownership tracking so an
ObservationArchiveis only registered as a child once.Various performance improvements (lazy imports to reduce memory consumption, a fast path for transit-window calculation).
[1.52.0] - 2026-06-13
Added
TransitImagingScriptfor imaging around a target’s transit.estimate_durationnow takestask/timeparameters and can compute full-transit durations.Added
ExposureTimeProvider(andStellarExposureTimeProvider) for dynamic per-exposure timing inImagingScript.Rewrote
pyobsdto no longer depend onstart-stop-daemon; prints subprocess stdout/stderr if a module fails to start.Added a GitHub Actions integration-test run triggered on release.
Removed the tornado-based logger handling in favor of slixmpp’s own logging.
The scheduler now only fetches pending and in-progress observations rather than everything.
[1.51.0] - 2026-06-09
Script.can_runcan now report a reason viacant_run_reason, surfaced by the mastermind when skipping a task.
[1.50.0] - 2026-06-09
Moved the robotic storage backends (filesystem, HTTP backend, LCO) into their own
pyobs.robotic.storagesubpackage.Added an in-memory
TaskArchive/ObservationArchiveimplementation, useful for testing.Added
Comm.has_module.Timeis now imported frompyobs.utils.timeinstead ofastropy.timethroughout.Added
Constraint.filter_skycoordfor faster constraint filtering.Fixed a proxy deduplication bug.
[1.49.0] - 2026-06-08
Added a
timeparameter toObservationArchive.get_scheduleandget_current_observationacross all backends (filesystem, HTTP backend, LCO).Log timestamps are now formatted as ISO 8601.
[1.48.0] - 2026-06-07
Switched linting from flake8 to ruff and fixed the resulting warnings project-wide.
Replaced broad
except Exceptioncatches with narrower exception handling in several places.Changed log calls to use lazy (
%-style) argument evaluation throughout.Renamed
ensure_featuretocreate_task.
[1.47.0] - 2025-06-07
Set minimum Python version to 3.11.
Replaced old-style type hints (
Optional,Union,List, etc.) with modern Python 3.11+ syntax throughout.Replaced deprecated
asyncio.ensure_futurewithasyncio.create_task.Replaced deprecated
asyncio.get_event_loopwithasyncio.get_running_loop.Replaced
str, EnumwithStrEnumthroughout.Replaced
asyncio.gatherwithasyncio.TaskGroupwhere applicable.Added ruff linting to CI and pre-commit.
[1.46.0] - 2025-05-27
Added
DynamicTargetclass for runtime target selection via a pluggablePickerinterface.Added
CsvPickerfor selecting targets from a CSV catalogue, with constraint-aware filtering.OnDemandSchedulernow resolves dynamic targets before evaluating constraints and merits.Added
directionparameter toSolarElevationConstraintfor filtering on rising or setting sun.Resolved target is now stored on
Observationand restored by the mastermind viafetch_task.Target resolution result is cached per scheduling run.
Added
set_resolved_targetandestimate_durationtoTask.LcoTaskRunnernow correctly injects script beforerun_task.Added missing abstract method implementations to
LcoTaskArchiveandLcoObservationArchive.Fixed inverted
PENDINGfilter inLcoTaskArchive.get_schedulable_tasks.Fixed
LcoConfiguration.statedefault to"PENDING".Portal now creates aiohttp session in
open()instead of__init__().Portal now managed via
add_child_objectfor correct lifecycle.Fixed
SolarElevationConstraintdirection logic usingobserver.midnight.Added
AstroplanSchedulertests; fixed empty block list causing subprocess hang.Fixed
LcoRequest.locationfield name conflict withBaseModel.location.Added
ImagingScriptfor standard science exposures with acquisition and guiding support.Comprehensive test suite additions for LCO classes, scheduler, and scripts.
[1.45.0] - 2025-05-14
Added unified
get_observationsinterface toObservationArchivewith state and time filters.IntervalMeritandPerNightMeritnow push filters directly toget_observations.Added
ObservationArchiveEvolution.get_observationsmethod.Added
min_safety_timeparameter toMastermind.Added
activefield toTask.Fixed telescope not stopping after auto focus.
[1.44.0] - 2025-04-24
Renamed
SubClassBaseModeltoPolymorphicBaseModel.Renamed
MeritSchedulertoOnDemandScheduler.Moved
utils.archiveandutils.skyflatsintorobotic.utilsas pydantic models.Moved
serialization.pyfromrobotic.utilstoutils.Converted
SkyFlatsBasePointing,SkyflatPriorities,Archive, andPyobsArchivetoPolymorphicBaseModel.Removed
ObjectfromScriptbase classes.Context injection now propagates to all child pydantic models.
HTTP requests now use pagination; all list responses wrapped in
{"results": [...]}format.Added
create_scriptmethod toTask.Added
http_request_with_retrieswith tenacity-based retry logic.BaseModelno longer inherits fromObject.Added
trigger_on_every_updateparameter toSchedulermodule.Added Observation priority field.
Major documentation overhaul for the robotic subsystem.
[1.43.0] - 2025-04-20
Added
http_request_with_retriesutility.Backend archives now use
http_request_with_retriesinstead of direct HTTP calls.Added
ignore_cert_errorsparameter to backend archives.Added filesystem-based backend for robotic scheduling (
YamlTaskArchive,YamlObservationArchive).Fixed
inject_class_on_serializationfor pydantic models.
[1.42.0] - 2025-03-08
Backend archives migrated from
requeststoaiohttp.Observation fetching moved to a background task.
Task changes now driven by
on_tasks_changedcallback.Refactored robotic subsystem to support pluggable backends.
[1.41.0] - 2025-02-20
Added optional Qt widget dependencies for GUI support.
Added general-purpose Qt widgets.
[1.40.0] - 2025-02-10
XMPP communication now connects with TLS using slixmpp 1.4.1.
Fixed PyPy compatibility issues.
Added
AvoidMoonconstraint.Added
FromListgrid filter.PerNightMeritnow uses last sunrise for night boundary.Auto focus now uses actual measured focus position.
[1.39.0] - 2024-12-10
Major scheduler refactoring: introduced pluggable scheduler architecture.
Added
OnDemandScheduler(merit-based greedy scheduler) withDataProvidercontext.Added
ABORTED,IN_PROGRESS, andFAILEDobservation states.Added
ConfigurationSummaryto LCO integration.
[1.38.0] - 2024-11-05
Added config file support via
--configflag.Added InfluxDB logging handler.
Added
CommLoggingHandlerguard against duplicate logger registration.Added
--verboseCLI switch.
[1.37.0] - 2024-10-20
Added InfluxDB logging handler (
InfluxLoggingHandler).
[1.36.0] - 2024-10-05
Improved exception handling in background tasks;
BackgroundTasknow receives parent reference.Unknown remote exceptions now wrapped in
RemoteError.Added SSL check option for XMPP.
Fixed: don’t reconnect after intentional XMPP disconnect.
Added example systemd service file.
[1.35.0] - 2024-09-01
Added
--verboseswitch to CLI.Added
CALIBRATINGmotion status.Refactored shell command handling into dedicated classes.
[1.34.0] - 2024-08-20
Refactored shell command and response into dedicated
ShellCommandclasses.
[1.33.0] - 2024-08-05
Added new grid creation for pointing series.
Configurable wait times in pointing module.
[1.32.0] - 2024-07-20
Moved exceptions to a dedicated module.
Added
AcquisitionErrorfor failed acquisitions.Changed
GeneralErrortoFocusErrorfor focus-related errors.Added 60s timeout to autoguider stop method.
LCO integration: use ConfigDB to fetch instrument; support multiple configurations.
[1.31.0] - 2024-05-25
Added documentation for image processors.
Registered
pyobsexceptions now logged asINFOwithout stack trace.Added
overwriteparameter to image writing.Added Matrix chat client module.
[1.30.0] - 2024-05-01
Added Matrix chat client module.
Fixed context propagation for non-dict objects.
Added reset method to
BrightestStarGuiding.Added
oneshotparameter toScriptRunner.Added logging when a script cannot run.
[1.29.0] - 2024-03-15
Added
PipelineCamerafor running image pipeline on camera images.Added
Flipimage processor.Added TypeScript interface export.
Added
HttpServerto serve images via HTTP.Added
SolarHelioprojectiveimage processor.Added
Pipelinemodule.
[1.28.0] - 2024-02-20
Added offset parameter to
IGain.Reduced httpx logging verbosity.
[1.27.0] - 2024-02-01
FilenameFormatternow supports formatting with FITS headers viaGetFitsHeaders.
[1.26.0] - 2024-01-15
Added comprehensive typing throughout using
TypedDictandnpt.NDArray.Improved
get_objectoverloads.
[1.25.0] - 2024-01-01
Migrated build system to
uv.Pointing module updated to use new grid classes.
[1.24.0] - 2023-12-30
Added new spherical grid classes (
RegularSphericalGrid, filters, pipeline) with tests.
[1.23.0] - 2023-12-29
Improved spilled light guiding sigmoid function for more accurate relative offset calculation.
Added weather station workaround.
[1.22.0] - 2023-12-29
Added
auto_focusintegration to relevant modules.
[1.21.0] - 2023-12-28
Added
init_offset_to_zerofunctionality.Spilled light guiding: added binning correction, image trimming, and improved calculations.
[1.20.0] - 2023-12-28
VFS not created if none is configured.
Added pixel offset image processor for simplified acquisition workflow.
get_objectno longer overwrites existing parameters.
[1.19.0] - 2023-12-28
Added spilled light guiding processor.
Added
IMultiFiberinterface.
[1.18.0] - 2023-12-28
ITelescopeno longer directly implementsIPointingRaDec/IPointingAltAz;BaseTelescopehandles dispatch.Added bright star guiding.
Restore initial focus if focus series fails.
[1.17.0] - 2023-12-28
Added
DummyModemodule andModeChangedEvent.Added
ResolvableErrorLoggerutility.Reactivated publisher module.
Added XMPP auto-reconnect on connection loss.
[1.16.0] - 2023-12-28
New
pyobsddaemon with improved module management and config testing.Added several default scripts.
Added group support for module configuration.
[1.15.0] - 2023-12-28
Added group support for module configuration.
[1.14.0] - 2023-12-28
Stop telescope after autofocus completes.
Updated to Python 3.12; replaced deprecated
datetime.utcnow().Warn when no RA/Dec given for pointing.
[1.13.0] - 2023-12-28
Added acquisition support for bright central star.
Added derotator offset handling.
Added
BackgroundTaskclass to simplify background task management inObject.Added unit tests for
BackgroundTaskandObject.
[1.12.0] - 2023-12-28
Added list command for pyobsd, which outputs all configurations.
Added bash auto-complete script pyobsd.
Added timeouts (to be defined in the config) for ScriptRunner modules.
[1.11.0] - 2023-12-25
Acquisition and AutoFocus both got a broadcast option to disable broadcast of images.
AutoFocus got a final_image parameter to take a final image at optimal focus.
[1.10.0] - 2023-12-24
Added CallModule script.
Changed ScriptRunner module so that it can run a script multiple times.
[1.9.0] - 2023-12-23
Added getters and safe getters for Image class.
[1.3.0] - 2023-02-04
Adopted LCO default task to new LCO portal.
[1.2.0] - 2022-10-06
Added AltAzOffsets and RaDecOffsets and (partly) implemented them in the ApplyOffsets classes.
[1.1.0] - 2022-09-20
Changed signature of pyobs.robotic.TaskSchedule.get_schedule to have no parameters.
[1.0.0] - 2022-09-13
[0.22.0] - 2022-08-25
Removed comm.sleexxmpp implementation.
Renamed comm.slixmpp to comm.xmpp.
[0.21.0] - 2022-08-25
Some pipeline stuff.
Added DbusComm for communicating via Dbus.
Cleaned up parameter casting for communication.
[0.20.0] - 2022-06-22
Some fixes with asyncio and the GUI.
Handle JID conflicts in XMPP.
[0.19.0] - 2022-05-17
Getter/setter methods in Module must be async.
get_task() in TaskScheduler is now async.
Lots of bug fixes.
[0.18.0] - 2022-03-13
New IGain interface.
[0.17.0] - 2022-02-14
Restructuring robotic system.
[0.16.0] - 2022-01-14
Added new exceptions.
Use those new exceptions to keep track of errors over time and raise SevereErrors.
Add new state to module, so that a severe error can put a module into an error state.
Added get_state() and get_error_string() methods to modules.
[0.15.0] - 2021-12-29
Added Comm implementation for SliXMPP (which should now be default) and moved old comm.xmpp to comm.sleekcmpp.
Using asyncio throughout the project, all method and event handlers are async now, as well as open/close methods.
Got rid of multi-threading as best as possible.
VFS now also uses asyncio.
[0.14.2]
Fixed a bug with Poetry
[0.14.1]
Added possibility to use class hierarchy for events, i.e. subscribe to a class and receive all derived events.
Change to Poetry as build system
[0.14] - 2021-11-03
Guiding modules accept a pipeline now, so more image processors than just Offsets can run.
Renamed ICameraBinning, ICameraExposureTime and ICameraWindow and removed the “Camera” part.
Added meta attribute (temporary storage, not I/O persistent) to Image.
Extracted IImageGrabber from ICamera and renamed expose() to grab_image().
Added new IVideo interface and a corresponding BaseVideo module.
Raising exception, if XmppComm cannot connect to server, allowing for graceful exit.
On shutdown, wait for hanging threads, and kill them after 30 seconds.
Multi-processing for the pipeline, using ccdproc now.
New interface IPointingSeries, giving access to methods at the telescope that support pointing series.
Send logs in thread.
Added concept of image processors that take an Image as parameter and return it after some processing.
Added new NStarOffsets image processor (T. Masur).
Improved scheduler.
Added pipelines that take a list of image processors (see Pipeline mixin).
Re-organized all get_object methods.
Improved type hints throughout the code.
Renamed all coordinated interfaces (IRaDec, etc) to IPointing*, i.e. IPointingRaDec.
Renamed all offset interfaces to IOffsets*, i.e. IOffsetsRaDec.
Renamed IFitsHeaderProvider to IFitsHeaderBefore and also renamed its only method.
Added IFitsHeaderAfter to fetch FITS headers after an exposure as well.
Moved functionality from Module to Object.
New meta data system for images.
Renamed IStoppable to IStartStop.
Added new proxy interfaces in interfaces.proxies. All proxies now derive from these interfaces instead of the original ones.
And a lot more cleanup and re-organization.
[0.13] - 2021-04-30
Added a Telegram bot module.
Added a module for a Kiosk mode, in which pictures are published on a webpage.
Added new IImageFormats interface for cameras that support multiple ones (e.g. grayscale and color).
Moved more enums into utils.enums, like WeatherSensors and MotionStatus.
Added list_binnings() to IBinning interface and (temporary) default implementation in BaseCamera.
Restructured image processors into pyobs.image.processors.
Split photometry into separate SourceDetection and Photometry interfaces, added DaophotSourceDetection, and PhotUtilsPhotometry.
Sending events non-blocking, which might solve some problems with disappeared XMPP clients.
Added lots of documentation, which included setting __module__ for many classes.
[0.12] - 2021-01-01
Changed PyObsModule to Module.
Removed possibility for network configs.
Added MultiModule, which allows for multiple modules in one process.
Flat scheduler: add options for readout times.
New OnlineReduction module for reduction during the night.
Fixed bug that sometimes appears in the interface caching for Comm.
LcoTaskArchive: added MoonSeparationConstraint, fixed AirmassConstraint.
Optimized Scheduler by only scheduling blocks that actually have a window in the given range.
Added module Seeing that extracts FWHMs from the catalogs in reduced images and calculated a median seeing.
Introduced concept of Publishers, which can be used to publish data to log, CSV, and hopefully later, database, web, etc.
Created new Object class that handles most of what Module did before so that Module only adds module specific stuff.
Added some convenience methods for reading/writing files to VFS.
Added new IConfig interface which is implemented in every module and allows remote access to config parameters (if getter/setters are implemented).
Removed count parameter from ICamera.expose().
Removed exposure_time parameter from ICamera.expose() and introduced IExposureTime interface.
Removed image_type parameter from ICamera.expose() and introduced IImageType.
Moved ImageType enumerator from ICamera to utils.enums.
[0.11] - 2020-10-18
Major changes to robotic system based on LCO portal.
Setting filter/window/binning in acquisition.
Added WaitForMotion and Follow mixins.
Added support for flats that don’t directly scale with binning.
New module for acoustic warning when autonomous modules are running.
Improved SepPhotometry by calculating columns used also by LCO.
New interface for Lat/Lon telescopes, e.g. solar telescopes.
[0.10] - 2020-05-05
Re-factored acquisition modules and added one based on astrometry.
Added combine_binnings parameter to FlatFielder, which triggers, whether to use one function for all binnings or not
Added get_current_weather() to IWeather
New FlatFieldPointing module that can move telescope to a flatfield pointing
Changed requirements in setup.py and put packages that are only required by a server module into [full]
Removed HTTP proxy classes
Some new mixins
[0.9] - 2020-03-06
working on robotic system based on LCO portal
[0.8] - 2019-11-17
Added module for bright star acquisition.
Added and changed some FITS header keywords.
Added module for flat-fielding.
Changed some interfaces.
Added basic pipeline.
Started with code that will be used for a full robotic mode.
Re-organized auto-guiding modules.
and many more…