citrascope 0.6.1__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. citrascope/api/abstract_api_client.py +14 -0
  2. citrascope/api/citra_api_client.py +41 -0
  3. citrascope/citra_scope_daemon.py +97 -38
  4. citrascope/hardware/abstract_astro_hardware_adapter.py +144 -8
  5. citrascope/hardware/adapter_registry.py +10 -3
  6. citrascope/hardware/devices/__init__.py +17 -0
  7. citrascope/hardware/devices/abstract_hardware_device.py +79 -0
  8. citrascope/hardware/devices/camera/__init__.py +13 -0
  9. citrascope/hardware/devices/camera/abstract_camera.py +102 -0
  10. citrascope/hardware/devices/camera/rpi_hq_camera.py +353 -0
  11. citrascope/hardware/devices/camera/usb_camera.py +402 -0
  12. citrascope/hardware/devices/camera/ximea_camera.py +744 -0
  13. citrascope/hardware/devices/device_registry.py +273 -0
  14. citrascope/hardware/devices/filter_wheel/__init__.py +7 -0
  15. citrascope/hardware/devices/filter_wheel/abstract_filter_wheel.py +73 -0
  16. citrascope/hardware/devices/focuser/__init__.py +7 -0
  17. citrascope/hardware/devices/focuser/abstract_focuser.py +78 -0
  18. citrascope/hardware/devices/mount/__init__.py +7 -0
  19. citrascope/hardware/devices/mount/abstract_mount.py +115 -0
  20. citrascope/hardware/direct_hardware_adapter.py +787 -0
  21. citrascope/hardware/filter_sync.py +94 -0
  22. citrascope/hardware/indi_adapter.py +6 -2
  23. citrascope/hardware/kstars_dbus_adapter.py +67 -96
  24. citrascope/hardware/nina_adv_http_adapter.py +81 -64
  25. citrascope/hardware/nina_adv_http_survey_template.json +4 -4
  26. citrascope/settings/citrascope_settings.py +25 -0
  27. citrascope/tasks/runner.py +105 -0
  28. citrascope/tasks/scope/static_telescope_task.py +17 -12
  29. citrascope/tasks/task.py +3 -0
  30. citrascope/time/__init__.py +13 -0
  31. citrascope/time/time_health.py +96 -0
  32. citrascope/time/time_monitor.py +164 -0
  33. citrascope/time/time_sources.py +62 -0
  34. citrascope/web/app.py +274 -51
  35. citrascope/web/static/app.js +379 -36
  36. citrascope/web/static/config.js +448 -108
  37. citrascope/web/static/filters.js +55 -0
  38. citrascope/web/static/style.css +39 -0
  39. citrascope/web/templates/dashboard.html +176 -36
  40. {citrascope-0.6.1.dist-info → citrascope-0.8.0.dist-info}/METADATA +17 -1
  41. citrascope-0.8.0.dist-info/RECORD +62 -0
  42. citrascope-0.6.1.dist-info/RECORD +0 -41
  43. {citrascope-0.6.1.dist-info → citrascope-0.8.0.dist-info}/WHEEL +0 -0
  44. {citrascope-0.6.1.dist-info → citrascope-0.8.0.dist-info}/entry_points.txt +0 -0
  45. {citrascope-0.6.1.dist-info → citrascope-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,94 @@
1
+ """Filter synchronization utilities for syncing hardware filters to backend API."""
2
+
3
+
4
+ def extract_enabled_filter_names(filter_config: dict) -> list[str]:
5
+ """Extract names of enabled filters from hardware configuration.
6
+
7
+ Args:
8
+ filter_config: Dict mapping filter IDs to config dicts with 'name' and 'enabled' keys
9
+
10
+ Returns:
11
+ List of filter names where enabled=True
12
+ """
13
+ enabled_names = []
14
+ for filter_id, config in filter_config.items():
15
+ if config.get("enabled", False):
16
+ enabled_names.append(config["name"])
17
+ return enabled_names
18
+
19
+
20
+ def build_spectral_config_from_expanded(expanded_filters: list[dict]) -> tuple[dict, list[str]]:
21
+ """Build discrete spectral_config from API expanded filter response.
22
+
23
+ Args:
24
+ expanded_filters: List of filter dicts from /filters/expand API response,
25
+ each with 'name', 'central_wavelength_nm', 'bandwidth_nm', 'is_known'
26
+
27
+ Returns:
28
+ Tuple of (spectral_config dict, list of unknown filter names)
29
+ """
30
+ filter_specs = []
31
+ unknown_filters = []
32
+
33
+ for f in expanded_filters:
34
+ filter_specs.append(
35
+ {"name": f["name"], "central_wavelength_nm": f["central_wavelength_nm"], "bandwidth_nm": f["bandwidth_nm"]}
36
+ )
37
+ if not f.get("is_known", True):
38
+ unknown_filters.append(f["name"])
39
+
40
+ spectral_config = {"type": "discrete", "filters": filter_specs}
41
+
42
+ return spectral_config, unknown_filters
43
+
44
+
45
+ def sync_filters_to_backend(api_client, telescope_id: str, filter_config: dict, logger) -> bool:
46
+ """Sync enabled filters from hardware to backend API.
47
+
48
+ Extracts enabled filter names, expands them via filter library API,
49
+ builds spectral_config, and updates telescope record.
50
+
51
+ Args:
52
+ api_client: CitraApiClient instance
53
+ telescope_id: UUID string of telescope to update
54
+ filter_config: Hardware filter configuration dict
55
+ logger: Logger instance for output
56
+
57
+ Returns:
58
+ True if sync succeeded, False otherwise
59
+ """
60
+ if not filter_config:
61
+ logger.debug("No filter configuration to sync")
62
+ return False
63
+
64
+ # Extract enabled filter names
65
+ enabled_filter_names = extract_enabled_filter_names(filter_config)
66
+
67
+ if not enabled_filter_names:
68
+ logger.debug("No enabled filters to sync")
69
+ return False
70
+
71
+ logger.info(f"Syncing {len(enabled_filter_names)} enabled filters to backend: {enabled_filter_names}")
72
+
73
+ # Expand filter names to full spectral specs via API
74
+ expand_response = api_client.expand_filters(enabled_filter_names)
75
+ if not expand_response or "filters" not in expand_response:
76
+ logger.warning("Failed to expand filter names - API returned no data")
77
+ return False
78
+
79
+ # Build spectral_config from expanded filters
80
+ expanded_filters = expand_response["filters"]
81
+ spectral_config, unknown_filters = build_spectral_config_from_expanded(expanded_filters)
82
+
83
+ if unknown_filters:
84
+ logger.warning(f"Unknown filters (using defaults): {unknown_filters}")
85
+
86
+ # Update telescope spectral_config via PATCH
87
+ update_response = api_client.update_telescope_spectral_config(telescope_id, spectral_config)
88
+
89
+ if update_response:
90
+ logger.info(f"Successfully synced {len(spectral_config['filters'])} filters to backend")
91
+ return True
92
+ else:
93
+ logger.warning("Failed to update telescope spectral_config on backend")
94
+ return False
@@ -47,7 +47,7 @@ class IndiAdapter(PyIndi.BaseClient, AbstractAstroHardwareAdapter):
47
47
  # TetraSolver.high_memory()
48
48
 
49
49
  @classmethod
50
- def get_settings_schema(cls) -> list[SettingSchemaEntry]:
50
+ def get_settings_schema(cls, **kwargs) -> list[SettingSchemaEntry]:
51
51
  """
52
52
  Return a schema describing configurable settings for the INDI adapter.
53
53
  """
@@ -60,6 +60,7 @@ class IndiAdapter(PyIndi.BaseClient, AbstractAstroHardwareAdapter):
60
60
  "description": "INDI server hostname or IP address",
61
61
  "required": True,
62
62
  "placeholder": "localhost or 192.168.1.100",
63
+ "group": "Connection",
63
64
  },
64
65
  {
65
66
  "name": "port",
@@ -71,6 +72,7 @@ class IndiAdapter(PyIndi.BaseClient, AbstractAstroHardwareAdapter):
71
72
  "placeholder": "7624",
72
73
  "min": 1,
73
74
  "max": 65535,
75
+ "group": "Connection",
74
76
  },
75
77
  {
76
78
  "name": "telescope_name",
@@ -80,6 +82,7 @@ class IndiAdapter(PyIndi.BaseClient, AbstractAstroHardwareAdapter):
80
82
  "description": "Name of the telescope device (leave empty to auto-detect)",
81
83
  "required": False,
82
84
  "placeholder": "Telescope Simulator",
85
+ "group": "Devices",
83
86
  },
84
87
  {
85
88
  "name": "camera_name",
@@ -89,6 +92,7 @@ class IndiAdapter(PyIndi.BaseClient, AbstractAstroHardwareAdapter):
89
92
  "description": "Name of the camera device (leave empty to auto-detect)",
90
93
  "required": False,
91
94
  "placeholder": "CCD Simulator",
95
+ "group": "Devices",
92
96
  },
93
97
  ]
94
98
 
@@ -726,5 +730,5 @@ class IndiAdapter(PyIndi.BaseClient, AbstractAstroHardwareAdapter):
726
730
  def get_observation_strategy(self) -> ObservationStrategy:
727
731
  return ObservationStrategy.MANUAL
728
732
 
729
- def perform_observation_sequence(self, task_id, satellite_data) -> str:
733
+ def perform_observation_sequence(self, task, satellite_data) -> str:
730
734
  raise NotImplementedError
@@ -62,7 +62,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
62
62
  images_dir: Path to the images directory
63
63
  **kwargs: Configuration including bus_name, ccd_name, filter_wheel_name
64
64
  """
65
- super().__init__(images_dir=images_dir)
65
+ super().__init__(images_dir=images_dir, **kwargs)
66
66
  self.logger: logging.Logger = logger
67
67
  self.bus_name = kwargs.get("bus_name") or "org.kde.kstars"
68
68
  self.ccd_name = kwargs.get("ccd_name") or "CCD Simulator"
@@ -76,19 +76,6 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
76
76
  self.binning_y = kwargs.get("binning_y", 1)
77
77
  self.image_format = kwargs.get("image_format", "Mono")
78
78
 
79
- # Filter management
80
- self.filter_map: Dict[int, Dict[str, Any]] = {}
81
-
82
- # Pre-populate filter_map from saved settings (if any)
83
- # This will be merged with discovered filters in discover_filters()
84
- saved_filters = kwargs.get("filters", {})
85
- for filter_id, filter_data in saved_filters.items():
86
- # Convert string keys back to int for internal use
87
- try:
88
- self.filter_map[int(filter_id)] = filter_data
89
- except (ValueError, TypeError) as e:
90
- self.logger.warning(f"Invalid filter ID '{filter_id}' in settings, skipping: {e}")
91
-
92
79
  self.bus: dbus.SessionBus | None = None
93
80
  self.kstars: dbus.Interface | None = None
94
81
  self.ekos: dbus.Interface | None = None
@@ -97,7 +84,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
97
84
  self.scheduler: dbus.Interface | None = None
98
85
 
99
86
  @classmethod
100
- def get_settings_schema(cls) -> list[SettingSchemaEntry]:
87
+ def get_settings_schema(cls, **kwargs) -> list[SettingSchemaEntry]:
101
88
  """
102
89
  Return a schema describing configurable settings for the KStars DBus adapter.
103
90
  """
@@ -110,6 +97,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
110
97
  "description": "D-Bus service name for KStars (default: org.kde.kstars)",
111
98
  "required": False,
112
99
  "placeholder": "org.kde.kstars",
100
+ "group": "Connection",
113
101
  },
114
102
  {
115
103
  "name": "ccd_name",
@@ -119,6 +107,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
119
107
  "description": "Name of the camera device in your Ekos profile (check Ekos logs on connect for available devices)",
120
108
  "required": False,
121
109
  "placeholder": "CCD Simulator",
110
+ "group": "Devices",
122
111
  },
123
112
  {
124
113
  "name": "filter_wheel_name",
@@ -128,6 +117,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
128
117
  "description": "Name of the filter wheel device (leave empty if no filter wheel)",
129
118
  "required": False,
130
119
  "placeholder": "Filter Simulator",
120
+ "group": "Devices",
131
121
  },
132
122
  {
133
123
  "name": "optical_train_name",
@@ -137,6 +127,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
137
127
  "description": "Name of the optical train in your Ekos profile (check Ekos logs on connect for available trains)",
138
128
  "required": False,
139
129
  "placeholder": "Primary",
130
+ "group": "Devices",
140
131
  },
141
132
  {
142
133
  "name": "exposure_time",
@@ -148,6 +139,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
148
139
  "placeholder": "1.0",
149
140
  "min": 0.001,
150
141
  "max": 300.0,
142
+ "group": "Imaging",
151
143
  },
152
144
  {
153
145
  "name": "frame_count",
@@ -159,6 +151,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
159
151
  "placeholder": "1",
160
152
  "min": 1,
161
153
  "max": 100,
154
+ "group": "Imaging",
162
155
  },
163
156
  {
164
157
  "name": "binning_x",
@@ -170,6 +163,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
170
163
  "placeholder": "1",
171
164
  "min": 1,
172
165
  "max": 4,
166
+ "group": "Imaging",
173
167
  },
174
168
  {
175
169
  "name": "binning_y",
@@ -181,6 +175,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
181
175
  "placeholder": "1",
182
176
  "min": 1,
183
177
  "max": 4,
178
+ "group": "Imaging",
184
179
  },
185
180
  {
186
181
  "name": "image_format",
@@ -191,6 +186,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
191
186
  "required": False,
192
187
  "placeholder": "Mono",
193
188
  "options": ["Mono", "RGGB", "RGB"],
189
+ "group": "Imaging",
194
190
  },
195
191
  ]
196
192
 
@@ -236,7 +232,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
236
232
  raise FileNotFoundError(f"Template not found: {template_path}")
237
233
  return template_path.read_text()
238
234
 
239
- def _create_sequence_file(self, task_id: str, satellite_data: dict, output_dir: Path) -> Path:
235
+ def _create_sequence_file(self, task_id: str, satellite_data: dict, output_dir: Path, task=None) -> Path:
240
236
  """
241
237
  Create an ESQ sequence file from template.
242
238
 
@@ -244,6 +240,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
244
240
  task_id: Unique task identifier
245
241
  satellite_data: Dictionary containing target information
246
242
  output_dir: Base output directory for captures
243
+ task: Optional task object containing filter assignment
247
244
 
248
245
  Returns:
249
246
  Path to the created sequence file
@@ -254,7 +251,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
254
251
  target_name = satellite_data.get("name", "Unknown").replace(" ", "_")
255
252
 
256
253
  # Generate job blocks based on filter configuration
257
- jobs_xml = self._generate_job_blocks(output_dir)
254
+ jobs_xml = self._generate_job_blocks(output_dir, task)
258
255
 
259
256
  # Replace placeholders
260
257
  sequence_content = template.replace("{{JOBS}}", jobs_xml)
@@ -274,13 +271,14 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
274
271
  self.logger.info(f"Created sequence file: {sequence_file}")
275
272
  return sequence_file
276
273
 
277
- def _generate_job_blocks(self, output_dir: Path) -> str:
274
+ def _generate_job_blocks(self, output_dir: Path, task=None) -> str:
278
275
  """
279
276
  Generate XML job blocks for each filter in filter_map.
280
277
  If no filters discovered, generates single job with no filter.
281
278
 
282
279
  Args:
283
280
  output_dir: Base output directory for captures
281
+ task: Optional task object containing filter assignment
284
282
 
285
283
  Returns:
286
284
  XML string containing one or more <Job> blocks
@@ -324,30 +322,31 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
324
322
 
325
323
  jobs = []
326
324
 
327
- if self.filter_map:
328
- # Multi-filter mode: create one job per discovered filter
329
- self.logger.info(
330
- f"Generating {len(self.filter_map)} jobs for filters: "
331
- f"{[f['name'] for f in self.filter_map.values()]}"
332
- )
333
- for filter_idx in sorted(self.filter_map.keys()):
334
- filter_info = self.filter_map[filter_idx]
335
- filter_name = filter_info["name"]
336
-
337
- job_xml = job_template.format(
338
- exposure=self.exposure_time,
339
- format=self.image_format,
340
- binning_x=self.binning_x,
341
- binning_y=self.binning_y,
342
- filter_name=filter_name,
343
- count=self.frame_count,
344
- output_dir=str(output_dir),
345
- )
346
- jobs.append(job_xml)
347
- else:
348
- # Single-filter mode: use '--' for no filter
325
+ # Select filters to use for this task (allow_no_filter for KStars '--' fallback)
326
+ filters_to_use = self.select_filters_for_task(task, allow_no_filter=True)
327
+
328
+ if filters_to_use is None:
329
+ # No filters available - use '--' for no filter wheel
349
330
  filter_name = "--" if not self.filter_wheel_name else "Luminance"
350
- self.logger.info(f"Generating single job with filter: {filter_name}")
331
+ task_id_str = task.id if task else "unknown"
332
+ self.logger.info(f"Using fallback filter '{filter_name}' for task {task_id_str}")
333
+
334
+ job_xml = job_template.format(
335
+ exposure=self.exposure_time,
336
+ format=self.image_format,
337
+ binning_x=self.binning_x,
338
+ binning_y=self.binning_y,
339
+ filter_name=filter_name,
340
+ count=self.frame_count,
341
+ output_dir=str(output_dir),
342
+ )
343
+ jobs.append(job_xml)
344
+ return "\n".join(jobs)
345
+
346
+ # Generate jobs for selected filters
347
+ for filter_idx in sorted(filters_to_use.keys()):
348
+ filter_info = filters_to_use[filter_idx]
349
+ filter_name = filter_info["name"]
351
350
 
352
351
  job_xml = job_template.format(
353
352
  exposure=self.exposure_time,
@@ -424,13 +423,15 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
424
423
 
425
424
  assert self.bus is not None
426
425
 
427
- # Calculate expected number of images based on filters
428
- expected_filter_count = len(self.filter_map) if self.filter_map else 1
429
- expected_total_images = expected_filter_count * self.frame_count
426
+ # Calculate expected number of images based on enabled filters
427
+ enabled_filter_count = (
428
+ sum(1 for f in self.filter_map.values() if f.get("enabled", True)) if self.filter_map else 1
429
+ )
430
+ expected_total_images = enabled_filter_count * self.frame_count
430
431
 
431
432
  self.logger.info(
432
433
  f"Waiting for scheduler job completion (timeout: {timeout}s, "
433
- f"expecting {expected_total_images} images across {expected_filter_count} filters)..."
434
+ f"expecting {expected_total_images} images across {enabled_filter_count} filters)..."
434
435
  )
435
436
  start_time = time.time()
436
437
 
@@ -529,12 +530,12 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
529
530
 
530
531
  return matching_files
531
532
 
532
- def perform_observation_sequence(self, task_id: str, satellite_data: dict) -> list[str]:
533
+ def perform_observation_sequence(self, task, satellite_data: dict) -> list[str]:
533
534
  """
534
535
  Execute a complete observation sequence using Ekos Scheduler.
535
536
 
536
537
  Args:
537
- task_id: Unique task identifier
538
+ task: Task object containing id and filter assignment
538
539
  satellite_data: Dictionary with keys: 'name', and either 'ra'/'dec' or TLE data
539
540
 
540
541
  Returns:
@@ -558,7 +559,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
558
559
  output_dir.mkdir(exist_ok=True, parents=True)
559
560
 
560
561
  # Clear task-specific directory to prevent Ekos from thinking job is already done
561
- task_output_dir = output_dir / task_id
562
+ task_output_dir = output_dir / task.id
562
563
  if task_output_dir.exists():
563
564
  shutil.rmtree(task_output_dir)
564
565
  self.logger.info(f"Cleared existing output directory: {task_output_dir}")
@@ -568,20 +569,20 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
568
569
  self.logger.info(f"Output directory: {task_output_dir}")
569
570
 
570
571
  # Create sequence and scheduler job files (use task-specific directory)
571
- sequence_file = self._create_sequence_file(task_id, satellite_data, task_output_dir)
572
- job_file = self._create_scheduler_job(task_id, satellite_data, sequence_file)
572
+ sequence_file = self._create_sequence_file(task.id, satellite_data, task_output_dir, task)
573
+ job_file = self._create_scheduler_job(task.id, satellite_data, sequence_file)
573
574
 
574
575
  # Ensure temp files are cleaned up even on failure
575
576
  try:
576
- self._execute_observation(task_id, output_dir, sequence_file, job_file)
577
+ self._execute_observation(task.id, output_dir, sequence_file, job_file)
577
578
  finally:
578
579
  # Cleanup temp files
579
580
  self._cleanup_temp_files(sequence_file, job_file)
580
581
 
581
582
  # Retrieve and return captured images
582
- image_paths = self._retrieve_captured_images(task_id, output_dir)
583
+ image_paths = self._retrieve_captured_images(task.id, output_dir)
583
584
  if not image_paths:
584
- raise RuntimeError(f"No images captured for task {task_id}")
585
+ raise RuntimeError(f"No images captured for task {task.id}")
585
586
 
586
587
  self.logger.info(f"Observation sequence complete: {len(image_paths)} images captured")
587
588
  return image_paths
@@ -858,14 +859,16 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
858
859
  # Use 0-based indexing for filter_map (slot 1 -> index 0)
859
860
  filter_idx = slot_num - 1
860
861
 
861
- # If filter already in map (from saved settings), preserve focus position
862
+ # If filter already in map (from saved settings), preserve focus position and enabled state
862
863
  if filter_idx in self.filter_map:
863
864
  focus_position = self.filter_map[filter_idx].get("focus_position", 0)
865
+ enabled = self.filter_map[filter_idx].get("enabled", True)
864
866
  self.logger.debug(
865
- f"Filter slot {slot_num} ({filter_name}): using saved focus position {focus_position}"
867
+ f"Filter slot {slot_num} ({filter_name}): using saved focus position {focus_position}, enabled: {enabled}"
866
868
  )
867
869
  else:
868
870
  focus_position = 0
871
+ enabled = True # Default new filters to enabled
869
872
  self.logger.debug(
870
873
  f"Filter slot {slot_num} ({filter_name}): new filter, using default focus position"
871
874
  )
@@ -873,6 +876,7 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
873
876
  self.filter_map[filter_idx] = {
874
877
  "name": filter_name,
875
878
  "focus_position": focus_position,
879
+ "enabled": enabled,
876
880
  }
877
881
  except Exception as e:
878
882
  self.logger.warning(f"Could not read filter slot {slot_num}: {e}")
@@ -888,54 +892,21 @@ class KStarsDBusAdapter(AbstractAstroHardwareAdapter):
888
892
  self.logger.info(f"Filter discovery failed (non-fatal): {e}")
889
893
  # Leave filter_map empty, use single-filter mode
890
894
 
891
- def supports_filter_management(self) -> bool:
892
- """Indicates whether this adapter supports filter/focus management.
893
-
894
- Returns:
895
- bool: True if filters were discovered, False otherwise.
896
- """
897
- return bool(self.filter_map)
898
-
899
- def get_filter_config(self) -> Dict[str, Dict[str, Any]]:
900
- """Get the current filter configuration including focus positions.
895
+ def supports_autofocus(self) -> bool:
896
+ """Indicates that KStars adapter does not support autofocus yet.
901
897
 
902
898
  Returns:
903
- dict: Dictionary mapping filter IDs (as strings) to FilterConfig.
904
- Each FilterConfig contains:
905
- - name (str): Filter name
906
- - focus_position (int): Focuser position for this filter
907
-
908
- Example:
909
- {
910
- "0": {"name": "Red", "focus_position": 9000},
911
- "1": {"name": "Green", "focus_position": 9050}
912
- }
899
+ bool: False (autofocus not implemented).
913
900
  """
914
- # Convert 0-based integer keys to strings for the web interface
915
- return {str(k): v for k, v in self.filter_map.items()}
916
-
917
- def update_filter_focus(self, filter_id: str, focus_position: int) -> bool:
918
- """Update the focus position for a specific filter.
901
+ return False
919
902
 
920
- Args:
921
- filter_id: Filter ID as string (0-based index)
922
- focus_position: New focus position in steps
903
+ def supports_filter_management(self) -> bool:
904
+ """Indicates whether this adapter supports filter/focus management.
923
905
 
924
906
  Returns:
925
- bool: True if update was successful, False otherwise
907
+ bool: True if filters were discovered, False otherwise.
926
908
  """
927
- try:
928
- idx = int(filter_id)
929
- if idx in self.filter_map:
930
- self.filter_map[idx]["focus_position"] = focus_position
931
- self.logger.info(f"Updated filter '{self.filter_map[idx]['name']}' focus position to {focus_position}")
932
- return True
933
- else:
934
- self.logger.warning(f"Filter ID {filter_id} not found in filter_map")
935
- return False
936
- except (ValueError, KeyError) as e:
937
- self.logger.error(f"Failed to update filter focus: {e}")
938
- return False
909
+ return bool(self.filter_map)
939
910
 
940
911
  def disconnect(self):
941
912
  raise NotImplementedError