Skip to content

Commit 8df93e5

Browse files
author
Jon
committed
Added settings to enable sending new structures and updates to instrument capabilities to HEROIC
1 parent e80de50 commit 8df93e5

8 files changed

Lines changed: 636 additions & 5 deletions

File tree

configdb/hardware/admin.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from reversion.admin import VersionAdmin
1212
from reversion.errors import RegistrationError
1313

14+
from configdb.hardware.heroic import update_heroic_instrument_capabilities
1415
from configdb.hardware.models import (
1516
Site, Enclosure, GenericMode, ModeType, GenericModeGroup, Telescope, Instrument, Camera, CameraType,
1617
OpticalElementGroup, OpticalElement, InstrumentType, InstrumentCategory, ConfigurationType, ConfigurationTypeProperties
@@ -165,12 +166,27 @@ class InstrumentAdmin(HardwareAdmin):
165166
def science_camera_codes(self, obj):
166167
return ','.join([science_camera.code for science_camera in obj.science_cameras.all()])
167168

169+
def save_related(self, request, form, formsets, change):
170+
# This is the best way to trigger on saving m2m admin relationships on the Instrument, like Science Cameras
171+
finished = super().save_related(request, form, formsets, change)
172+
update_heroic_instrument_capabilities(form.instance)
173+
return finished
174+
168175
@admin.register(Camera)
169176
class CameraAdmin(HardwareAdmin):
170177
form = CameraAdminForm
171178
list_display = ('code', 'camera_type')
172179
search_fields = ('code',)
173180

181+
def save_related(self, request, form, formsets, change):
182+
# This is the best way to trigger on saving m2m admin relationships on the Camera, like Optical Element Groups
183+
old_oegs = set(form.instance.optical_element_groups.all())
184+
finished = super().save_related(request, form, formsets, change)
185+
if old_oegs != set(form.instance.optical_element_groups.all()):
186+
for instrument in form.instance.instrument_set.all():
187+
update_heroic_instrument_capabilities(instrument)
188+
return finished
189+
174190

175191
@admin.register(InstrumentType)
176192
class InstrumentTypeAdmin(HardwareAdmin):
@@ -193,13 +209,47 @@ class GenericModeGroupAdmin(HardwareAdmin):
193209
search_fields = ('instrument_type', 'type')
194210
list_filter = ('type', 'instrument_type')
195211

212+
def save_model(self, request, obj, form, change):
213+
old_instrument_type = None
214+
if obj.pk:
215+
old_obj = self.model.objects.get(pk=obj.pk)
216+
if old_obj.instrument_type != obj.instrument_type:
217+
old_instrument_type = old_obj.instrument_type
218+
# Now update the model so the new model details are saved
219+
finished = super().save_model(request, obj, form, change)
220+
221+
if old_instrument_type:
222+
# The instrument_type has changed so update heroic for the old instrument type
223+
for instrument in old_instrument_type.instrument_set.all():
224+
update_heroic_instrument_capabilities(instrument)
225+
return finished
226+
227+
def save_related(self, request, form, formsets, change):
228+
# This is the best way to trigger on saving m2m admin relationships on the GenericModeGroup,
229+
# like when its members change
230+
finished = super().save_related(request, form, formsets, change)
231+
if form.instance.instrument_type:
232+
for instrument in form.instance.instrument_type.instrument_set.all():
233+
update_heroic_instrument_capabilities(instrument)
234+
return finished
235+
196236

197237
@admin.register(GenericMode)
198238
class GenericModeAdmin(HardwareAdmin):
199239
form = GenericModeAdminForm
200240
list_display = ('name', 'code', 'overhead', 'schedulable')
201241
search_fields = ('name', 'code')
202242

243+
def save_related(self, request, form, formsets, change):
244+
# This is the best way to trigger on saving m2m admin relationships on the GenericMode,
245+
# like when its membersships change
246+
finished = super().save_related(request, form, formsets, change)
247+
for gmg in form.instance.genericmodegroup_set.all():
248+
if gmg.instrument_type:
249+
for instrument in gmg.instrument_type.instrument_set.all():
250+
update_heroic_instrument_capabilities(instrument)
251+
return finished
252+
203253

204254
@admin.register(OpticalElementGroup)
205255
class OpticalElementGroupAdmin(HardwareAdmin):
@@ -208,12 +258,31 @@ class OpticalElementGroupAdmin(HardwareAdmin):
208258
search_fields = ('name', 'type')
209259
list_filter = ('type',)
210260

261+
def save_related(self, request, form, formsets, change):
262+
# This is the best way to trigger on saving m2m admin relationships on the OpticalElementGroup,
263+
# like when its members change
264+
finished = super().save_related(request, form, formsets, change)
265+
for camera in form.instance.camera_set.all():
266+
for instrument in camera.instrument_set.all():
267+
update_heroic_instrument_capabilities(instrument)
268+
return finished
269+
211270

212271
@admin.register(OpticalElement)
213272
class OpticalElementAdmin(HardwareAdmin):
214273
list_display = ('name', 'code', 'schedulable')
215274
search_fields = ('name', 'code')
216275

276+
def save_related(self, request, form, formsets, change):
277+
# This is the best way to trigger on saving m2m admin relationships on the OpticalElement,
278+
# like when its memberships change
279+
finished = super().save_related(request, form, formsets, change)
280+
for oeg in form.instance.opticalelementgroup_set.all():
281+
for camera in oeg.camera_set.all():
282+
for instrument in camera.instrument_set.all():
283+
update_heroic_instrument_capabilities(instrument)
284+
return finished
285+
217286

218287
@admin.register(LogEntry)
219288
class LogEntryAdmin(admin.ModelAdmin):

configdb/hardware/apps.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.apps import AppConfig
2+
from django.conf import settings
3+
4+
5+
def can_submit_to_heroic():
6+
return settings.HEROIC_API_URL and settings.HEROIC_API_TOKEN and settings.HEROIC_OBSERVATORY
7+
8+
9+
class HardwareConfig(AppConfig):
10+
name = 'configdb.hardware'
11+
12+
def ready(self):
13+
# Only load the heroic communication signals if heroic settings are set
14+
if can_submit_to_heroic():
15+
import configdb.hardware.signals.handlers # noqa
16+
super().ready()

configdb/hardware/heroic.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
from django.conf import settings
2+
from urllib.parse import urljoin
3+
import requests
4+
import logging
5+
6+
from configdb.hardware.models import Instrument, Telescope, Site
7+
from configdb.hardware.apps import can_submit_to_heroic
8+
9+
10+
logger = logging.getLogger()
11+
12+
13+
def instrument_status_conversion(state: str):
14+
''' Converts instrument state to HEROIC instrument status
15+
'''
16+
if state == 'DISABLED' or state == 'MANUAL':
17+
return 'UNAVAILABLE'
18+
elif state == 'SCHEDULABLE':
19+
return 'SCHEDULABLE'
20+
else:
21+
return 'AVAILABLE'
22+
23+
24+
def telescope_status_conversion(telescope: Telescope):
25+
return 'SCHEDULABLE' if telescope.active and telescope.enclosure.active and telescope.enclosure.site.active else 'UNAVAILABLE'
26+
27+
28+
def heroic_site_id(site: Site):
29+
''' Extract a HEROIC id for the site.
30+
This concatenates the observatory.site for human readability
31+
'''
32+
return settings.HEROIC_OBSERVATORY + '.' + site.code
33+
34+
35+
def heroic_telescope_id(telescope: Telescope):
36+
''' Extract a HEROIC id for the telescope
37+
This concatenates the observatory.site.telescope for human readability
38+
'''
39+
return heroic_site_id(telescope.enclosure.site) + '.' + telescope.enclosure.code + '-' + telescope.code
40+
41+
42+
def heroic_instrument_id(instrument: Instrument):
43+
''' Extract a HEROIC id for the instrument
44+
This concatenates the observatory.site.telescope.instrument
45+
for human-readability.
46+
'''
47+
return heroic_telescope_id(instrument.telescope) + '.' + instrument.code
48+
49+
50+
def heroic_optical_element_groups(instrument: Instrument):
51+
''' Puts the optical element groups of an instrument in the format for reporting to HEROIC
52+
'''
53+
optical_element_groups = {}
54+
for camera in instrument.science_cameras.all():
55+
for optical_element_group in camera.optical_element_groups.all():
56+
optical_element_groups[optical_element_group.type] = {'options': []}
57+
if optical_element_group.default:
58+
optical_element_groups[optical_element_group.type]['default'] = optical_element_group.default.code
59+
for optical_element in optical_element_group.optical_elements.all():
60+
optical_element_groups[optical_element_group.type]['options'].append({
61+
'id': optical_element.code,
62+
'name': optical_element.name,
63+
'schedulable': optical_element.schedulable
64+
})
65+
return optical_element_groups
66+
67+
68+
def heroic_operation_modes(instrument: Instrument):
69+
''' Puts the generic mode groups of an instrument in the format for reporting to HEROIC
70+
'''
71+
operation_modes = {}
72+
for generic_mode_group in instrument.instrument_type.mode_types.all():
73+
operation_modes[generic_mode_group.type.id] = {'options': []}
74+
if generic_mode_group.default:
75+
operation_modes[generic_mode_group.type.id]['default'] = generic_mode_group.default.code
76+
for mode in generic_mode_group.modes.all():
77+
operation_modes[generic_mode_group.type.id]['options'].append({
78+
'id': mode.code,
79+
'name': mode.name,
80+
'schedulable': mode.schedulable
81+
})
82+
return operation_modes
83+
84+
85+
def instrument_to_heroic_instrument_capabilities(instrument: Instrument):
86+
''' Extracts the current instrument capabilities of an instrument to send to HEROIC
87+
'''
88+
capabilities = {
89+
'instrument': heroic_instrument_id(instrument),
90+
'status': instrument_status_conversion(instrument.state),
91+
'optical_element_groups': heroic_optical_element_groups(instrument),
92+
'operation_modes': heroic_operation_modes(instrument)
93+
}
94+
return capabilities
95+
96+
97+
def telescope_to_heroic_telescope_properties(telescope: Telescope):
98+
''' Extracts the current telescope properties of a telescope to send to HEROIC
99+
'''
100+
telescope_payload = {
101+
'name': f"{telescope.name} - {telescope.enclosure.name}",
102+
'site': heroic_site_id(telescope.enclosure.site),
103+
'aperture': telescope.aperture,
104+
'latitude': telescope.lat,
105+
'longitude': telescope.long,
106+
'horizon': telescope.horizon,
107+
'negative_ha_limit': telescope.ha_limit_neg,
108+
'positive_ha_limit': telescope.ha_limit_pos,
109+
'zenith_blind_spot': telescope.zenith_blind_spot
110+
}
111+
return telescope_payload
112+
113+
114+
def send_to_heroic(api_endpoint: str, payload: dict, update: bool = False):
115+
''' Function to send data to HEROIC API endpoints
116+
'''
117+
headers = {'Authorization': f'Token {settings.HEROIC_API_TOKEN}'}
118+
url = urljoin(settings.HEROIC_API_URL, api_endpoint)
119+
if update:
120+
response = requests.patch(url, headers=headers, json=payload)
121+
else:
122+
response = requests.post(url, headers=headers, json=payload)
123+
logger.warning(response.json())
124+
response.raise_for_status()
125+
126+
127+
128+
def create_heroic_instrument(instrument: Instrument):
129+
''' Create a new instrument payload and send it to HEROIC
130+
'''
131+
instrument_payload = {
132+
'id': heroic_instrument_id(instrument),
133+
'name': f"{instrument.instrument_type.name} - {instrument.code}",
134+
'telescope': heroic_telescope_id(instrument.telescope),
135+
'available': True
136+
}
137+
try:
138+
send_to_heroic('instruments/', instrument_payload)
139+
except Exception as e:
140+
logger.error(f'Failed to create heroic instrument {str(instrument)}: {repr(e)}')
141+
142+
143+
def update_heroic_instrument_capabilities(instrument: Instrument):
144+
''' Send the current instrument capabilities of an instrument to HEROIC
145+
if it is not DISABLED and heroic is set up in settings.py
146+
'''
147+
if can_submit_to_heroic() and instrument.state != 'DISABLED':
148+
capabilities = instrument_to_heroic_instrument_capabilities(instrument)
149+
try:
150+
send_to_heroic('instrument-capabilities/', capabilities)
151+
except Exception as e:
152+
logger.error(f'Failed to create heroic instrument {str(instrument)} capability update: {repr(e)}')
153+
154+
155+
def create_heroic_telescope(telescope: Telescope):
156+
''' Create a new telescope payload and send it to HEROIC
157+
'''
158+
telescope_payload = telescope_to_heroic_telescope_properties(telescope)
159+
telescope_payload['id'] = heroic_telescope_id(telescope)
160+
telescope_payload['status'] = telescope_status_conversion(telescope)
161+
if telescope_payload['status'] != 'SCHEDULABLE':
162+
telescope_payload['reason'] = 'Telescope is currently marked as inactive to prevent usage'
163+
try:
164+
send_to_heroic('telescopes/', telescope_payload)
165+
except Exception as e:
166+
logger.error(f'Failed to create heroic telescope {str(telescope)}: {repr(e)}')
167+
168+
169+
def update_heroic_telescope_properties(telescope: Telescope):
170+
''' Send updated telescope properties to HEROIC when they change
171+
'''
172+
telescope_update_payload = telescope_to_heroic_telescope_properties(telescope)
173+
try:
174+
send_to_heroic(f'telescopes/{heroic_telescope_id(telescope)}/', telescope_update_payload, update=True)
175+
except Exception as e:
176+
logger.error(f'Failed to update heroic telescope {str(telescope)}: {repr(e)}')
177+
178+
179+
def site_to_heroic_site_properties(site: Site):
180+
''' Extracts the current site properties of a site to send to HEROIC
181+
'''
182+
site_payload = {
183+
'name': site.name,
184+
'observatory': settings.HEROIC_OBSERVATORY,
185+
'elevation': site.elevation,
186+
'timezone': site.tz
187+
}
188+
return site_payload
189+
190+
191+
def create_heroic_site(site: Site):
192+
''' Create a new site payload and send it to HEROIC
193+
'''
194+
site_payload = site_to_heroic_site_properties(site)
195+
site_payload['id'] = heroic_site_id(site)
196+
try:
197+
send_to_heroic('sites/', site_payload)
198+
except Exception as e:
199+
logger.error(f'Failed to create heroic site {str(site)}: {repr(e)}')
200+
201+
202+
def update_heroic_site(site: Site):
203+
''' Send updated site properties to HEROIC when they change
204+
'''
205+
site_payload = site_to_heroic_site_properties(site)
206+
try:
207+
send_to_heroic(f'sites/{heroic_site_id(site)}/', site_payload, update=True)
208+
except Exception as e:
209+
logger.error(f'Failed to update heroic site {str(site)}: {repr(e)}')

0 commit comments

Comments
 (0)