1#!/usr/bin/env python
2
3import os
4import json
5import logging
6import argparse
7import socket
8from threading import Timer
9import subprocess
10
11from bottle import Bottle, run, template, static_file, request, response, BaseRequest, default_app
12from .indi_server import IndiServer, INDI_PORT, INDI_FIFO, INDI_CONFIG_DIR
13from .driver import DeviceDriver, DriverCollection, INDI_DATA_DIR
14from .database import Database
15from .device import Device
16from .indihub_agent import IndiHubAgent
17
18# default settings
19WEB_HOST = '0.0.0.0'
20WEB_PORT = 8624
21
22# Make it 10MB
23BaseRequest.MEMFILE_MAX = 50 * 1024 * 1024
24
25pkg_path, _ = os.path.split(os.path.abspath(__file__))
26views_path = os.path.join(pkg_path, 'views')
27
28parser = argparse.ArgumentParser(
29    description='INDI Web Manager. '
30    'A simple web application to manage an INDI server')
31
32parser.add_argument('--indi-port', '-p', type=int, default=INDI_PORT,
33                    help='indiserver port (default: %d)' % INDI_PORT)
34parser.add_argument('--port', '-P', type=int, default=WEB_PORT,
35                    help='Web server port (default: %d)' % WEB_PORT)
36parser.add_argument('--host', '-H', default=WEB_HOST,
37                    help='Bind web server to this interface (default: %s)' %
38                    WEB_HOST)
39parser.add_argument('--fifo', '-f', default=INDI_FIFO,
40                    help='indiserver FIFO path (default: %s)' % INDI_FIFO)
41parser.add_argument('--conf', '-c', default=INDI_CONFIG_DIR,
42                    help='INDI config. directory (default: %s)' % INDI_CONFIG_DIR)
43parser.add_argument('--xmldir', '-x', default=INDI_DATA_DIR,
44                    help='INDI XML directory (default: %s)' % INDI_DATA_DIR)
45parser.add_argument('--verbose', '-v', action='store_true',
46                    help='Print more messages')
47parser.add_argument('--logfile', '-l', help='log file name')
48parser.add_argument('--server', '-s', default='standalone',
49                    help='HTTP server [standalone|apache] (default: standalone')
50
51args = parser.parse_args()
52
53
54logging_level = logging.WARNING
55
56if args.verbose:
57    logging_level = logging.DEBUG
58
59if args.logfile:
60    logging.basicConfig(filename=args.logfile,
61                        format='%(asctime)s - %(levelname)s: %(message)s',
62                        level=logging_level)
63
64else:
65    logging.basicConfig(format='%(asctime)s - %(levelname)s: %(message)s',
66                        level=logging_level)
67
68logging.debug("command line arguments: " + str(vars(args)))
69
70hostname = socket.gethostname()
71
72collection = DriverCollection(args.xmldir)
73indi_server = IndiServer(args.fifo, args.conf)
74indi_device = Device()
75
76indihub_agent = IndiHubAgent('%s:%d' % (args.host, args.port), hostname, args.port)
77
78db_path = os.path.join(args.conf, 'profiles.db')
79db = Database(db_path)
80
81collection.parse_custom_drivers(db.get_custom_drivers())
82
83if args.server == 'standalone':
84    app = Bottle()
85    logging.info('using Bottle as standalone server')
86else:
87    app = default_app()
88    logging.info('using Apache web server')
89
90saved_profile = None
91active_profile = ""
92
93
94def start_profile(profile):
95    info = db.get_profile(profile)
96
97    profile_drivers = db.get_profile_drivers_labels(profile)
98    all_drivers = [collection.by_label(d['label']) for d in profile_drivers]
99
100    # Find if we have any remote drivers
101    remote_drivers = db.get_profile_remote_drivers(profile)
102    if remote_drivers:
103        drivers = remote_drivers['drivers'].split(',')
104        for drv in drivers:
105            all_drivers.append(DeviceDriver(drv, drv, "1.0", drv, "Remote"))
106
107    if all_drivers:
108        indi_server.start(info['port'], all_drivers)
109        # Auto connect drivers in 3 seconds if required.
110        if info['autoconnect'] == 1:
111            t = Timer(3, indi_server.auto_connect)
112            t.start()
113
114
115@app.route('/static/<path:path>')
116def callback(path):
117    """Serve static files"""
118    return static_file(path, root=views_path)
119
120
121@app.route('/favicon.ico', method='GET')
122def get_favicon():
123    """Serve favicon"""
124    return static_file('favicon.ico', root=views_path)
125
126
127@app.route('/')
128def main_form():
129    """Main page"""
130    global saved_profile
131    drivers = collection.get_families()
132
133    if not saved_profile:
134        saved_profile = request.get_cookie('indiserver_profile') or 'Simulators'
135
136    profiles = db.get_profiles()
137    return template(os.path.join(views_path, 'form.tpl'), profiles=profiles,
138                    drivers=drivers, saved_profile=saved_profile,
139                    hostname=hostname)
140
141###############################################################################
142# Profile endpoints
143###############################################################################
144
145
146@app.get('/api/profiles')
147def get_json_profiles():
148    """Get all profiles (JSON)"""
149    results = db.get_profiles()
150    return json.dumps(results)
151
152
153@app.get('/api/profiles/<item>')
154def get_json_profile(item):
155    """Get one profile info"""
156    results = db.get_profile(item)
157    return json.dumps(results)
158
159
160@app.post('/api/profiles/<name>')
161def add_profile(name):
162    """Add new profile"""
163    db.add_profile(name)
164
165
166@app.delete('/api/profiles/<name>')
167def delete_profile(name):
168    """Delete Profile"""
169    db.delete_profile(name)
170
171
172@app.put('/api/profiles/<name>')
173def update_profile(name):
174    """Update profile info (port & autostart & autoconnect)"""
175    response.set_cookie("indiserver_profile", name,
176                        None, max_age=3600000, path='/')
177    data = request.json
178    port = data.get('port', args.indi_port)
179    autostart = bool(data.get('autostart', 0))
180    autoconnect = bool(data.get('autoconnect', 0))
181    db.update_profile(name, port, autostart, autoconnect)
182
183
184@app.post('/api/profiles/<name>/drivers')
185def save_profile_drivers(name):
186    """Add drivers to existing profile"""
187    data = request.json
188    db.save_profile_drivers(name, data)
189
190
191@app.post('/api/profiles/custom')
192def save_profile_custom_driver():
193    """Add custom driver to existing profile"""
194    data = request.json
195    db.save_profile_custom_driver(data)
196    collection.clear_custom_drivers()
197    collection.parse_custom_drivers(db.get_custom_drivers())
198
199
200@app.get('/api/profiles/<item>/labels')
201def get_json_profile_labels(item):
202    """Get driver labels of specific profile"""
203    results = db.get_profile_drivers_labels(item)
204    return json.dumps(results)
205
206
207@app.get('/api/profiles/<item>/remote')
208def get_remote_drivers(item):
209    """Get remote drivers of specific profile"""
210    results = db.get_profile_remote_drivers(item)
211    if results is None:
212        results = {}
213    return json.dumps(results)
214
215
216###############################################################################
217# Server endpoints
218###############################################################################
219
220@app.get('/api/server/status')
221def get_server_status():
222    """Server status"""
223    status = [{'status': str(indi_server.is_running()), 'active_profile': active_profile}]
224    return json.dumps(status)
225
226
227@app.get('/api/server/drivers')
228def get_server_drivers():
229    """List server drivers"""
230    # status = []
231    # for driver in indi_server.get_running_drivers():
232    #     status.append({'driver': driver})
233    # return json.dumps(status)
234    # labels = []
235    # for label in sorted(indi_server.get_running_drivers().keys()):
236    #     labels.append({'driver': label})
237    # return json.dumps(labels)
238    drivers = []
239    if indi_server.is_running() is True:
240        for driver in indi_server.get_running_drivers().values():
241            drivers.append(driver.__dict__)
242    return json.dumps(drivers)
243
244
245@app.post('/api/server/start/<profile>')
246def start_server(profile):
247    """Start INDI server for a specific profile"""
248    global saved_profile
249    saved_profile = profile
250    global active_profile
251    active_profile = profile
252    response.set_cookie("indiserver_profile", profile,
253                        None, max_age=3600000, path='/')
254    start_profile(profile)
255
256
257@app.post('/api/server/stop')
258def stop_server():
259    """Stop INDI Server"""
260    indihub_agent.stop()
261    indi_server.stop()
262
263    global active_profile
264    active_profile = ""
265
266    # If there is saved_profile already let's try to reset it
267    global saved_profile
268    if saved_profile:
269        saved_profile = request.get_cookie("indiserver_profile") or "Simulators"
270
271
272###############################################################################
273# Driver endpoints
274###############################################################################
275
276@app.get('/api/drivers/groups')
277def get_json_groups():
278    """Get all driver families (JSON)"""
279    response.content_type = 'application/json'
280    families = collection.get_families()
281    return json.dumps(sorted(families.keys()))
282
283
284@app.get('/api/drivers')
285def get_json_drivers():
286    """Get all drivers (JSON)"""
287    response.content_type = 'application/json'
288    return json.dumps([ob.__dict__ for ob in collection.drivers])
289
290
291@app.post('/api/drivers/start/<label>')
292def start_driver(label):
293    """Start INDI driver"""
294    driver = collection.by_label(label)
295    indi_server.start_driver(driver)
296    logging.info('Driver "%s" started.' % label)
297
298
299@app.post('/api/drivers/stop/<label>')
300def stop_driver(label):
301    """Stop INDI driver"""
302    driver = collection.by_label(label)
303    indi_server.stop_driver(driver)
304    logging.info('Driver "%s" stopped.' % label)
305
306
307@app.post('/api/drivers/restart/<label>')
308def restart_driver(label):
309    """Restart INDI driver"""
310    driver = collection.by_label(label)
311    indi_server.stop_driver(driver)
312    indi_server.start_driver(driver)
313    logging.info('Driver "%s" restarted.' % label)
314
315###############################################################################
316# Device endpoints
317###############################################################################
318
319
320@app.get('/api/devices')
321def get_devices():
322    return json.dumps(indi_device.get_devices())
323
324###############################################################################
325# System control endpoints
326###############################################################################
327
328
329@app.post('/api/system/reboot')
330def system_reboot():
331    """reboot the system running indi-web"""
332    logging.info('System reboot, stopping server...')
333    stop_server()
334    logging.info('rebooting...')
335    subprocess.call('reboot')
336
337
338@app.post('/api/system/poweroff')
339def system_poweroff():
340    """poweroff the system"""
341    logging.info('System poweroff, stopping server...')
342    stop_server()
343    logging.info('poweroff...')
344    subprocess.run("poweroff")
345
346###############################################################################
347# INDIHUB Agent control endpoints
348###############################################################################
349
350
351@app.get('/api/indihub/status')
352def get_indihub_status():
353    """INDIHUB Agent status"""
354    mode = indihub_agent.get_mode()
355    is_running = indihub_agent.is_running()
356    response.content_type = 'application/json'
357    status = [{'status': str(is_running), 'mode': mode, 'active_profile': active_profile}]
358    return json.dumps(status)
359
360
361@app.post('/api/indihub/mode/<mode>')
362def change_indihub_agent_mode(mode):
363    """Change INDIHUB Agent mode with a current INDI-profile"""
364
365    if active_profile == "" or not indi_server.is_running():
366        response.content_type = 'application/json'
367        response.status = 500
368        return json.dumps({'message': 'INDI-server is not running. You need to run INDI-server first.'})
369
370    indihub_agent.stop()
371
372    if mode == 'off':
373        return
374
375    indihub_agent.start(active_profile, mode)
376
377
378###############################################################################
379# Startup standalone server
380###############################################################################
381
382
383def main():
384    """Start autostart profile if any"""
385    global active_profile
386
387    for profile in db.get_profiles():
388        if profile['autostart']:
389            start_profile(profile['name'])
390            active_profile = profile['name']
391            break
392
393    run(app, host=args.host, port=args.port, quiet=args.verbose)
394    logging.info("Exiting")
395
396
397# JM 2018-12-24: Added __main__ so I can debug this as a module in PyCharm
398# Otherwise, I couldn't get it to run main as all
399if __name__ == '__init__' or __name__ == '__main__':
400    main()
401