From 7729dca315ac0ff4438bd9bcd41b2cd38b052f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20Gru=CC=88ndger?= Date: Thu, 21 Dec 2017 22:43:22 +0100 Subject: [PATCH] Add backend for live.glidernet.org and ognrange.onglide.com (WIP) --- ogn/backend/__init__.py | 0 ogn/backend/liveglidernet.py | 93 +++++++++++++++++++++++ ogn/backend/ognrange.py | 35 +++++++++ setup.py | 3 +- tests/backend/test_backends.py | 131 +++++++++++++++++++++++++++++++++ 5 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 ogn/backend/__init__.py create mode 100644 ogn/backend/liveglidernet.py create mode 100644 ogn/backend/ognrange.py create mode 100644 tests/backend/test_backends.py diff --git a/ogn/backend/__init__.py b/ogn/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ogn/backend/liveglidernet.py b/ogn/backend/liveglidernet.py new file mode 100644 index 0000000..e4d4e11 --- /dev/null +++ b/ogn/backend/liveglidernet.py @@ -0,0 +1,93 @@ +from datetime import datetime, timedelta, date +import os + +from sqlalchemy import func, and_, between, case, null + +from ogn.model import AircraftBeacon, DeviceInfo, Device, Receiver + + +def rec(session): + last_10_minutes = datetime.utcnow() - timedelta(minutes=10) + receiver_query = session.query(Receiver, + case([(Receiver.lastseen > last_10_minutes, True)], + else_=False).label('is_online')) \ + .order_by(Receiver.name) + + lines = list() + lines.append('') + lines.append('') + lines.append('') + for [receiver, is_online] in receiver_query.all(): + lines.append('' + .format(receiver.name, receiver.location.latitude, receiver.location.longitude, is_online)) + + lines.append('') + xml = '\n'.join(lines) + + return xml + + +def lxml(session, show_offline=False, lat_max=90, lat_min=-90, lon_max=180, lon_min=-180): + + if show_offline: + observation_start = date.today() + else: + observation_start = datetime.utcnow() - timedelta(minutes=5) + + position_query = session.query(Device, AircraftBeacon, DeviceInfo) \ + .filter(and_(between(func.ST_Y(AircraftBeacon.location_wkt), lat_min, lat_max), + between(func.ST_X(AircraftBeacon.location_wkt), lon_min, lon_max))) \ + .filter(Device.lastseen > observation_start) \ + .filter(Device.last_position_beacon_id == AircraftBeacon.id) \ + .outerjoin(DeviceInfo, DeviceInfo.device_id == Device.id) + + lines = list() + lines.append('') + lines.append('') + + for [aircraft_beacon, device_info] in position_query.all(): + if device_info and (not device_info.tracked or not device_info.identified): + continue + + code = encode(aircraft_beacon.address) + + if device_info is None: + competition = ('_' + code[-2:]).lower() + registration = code + address = 0 + else: + if not device_info.competition: + competition = device_info.registration[-2:] + else: + competition = device_info.competition + + if not device_info.registration: + registration = '???' + else: + registration = device_info.registration + + address = device_info.address + + elapsed_time = datetime.utcnow() - aircraft_beacon.timestamp + elapsed_seconds = int(elapsed_time.total_seconds()) + + lines.append('' + .format(aircraft_beacon.location.latitude, + aircraft_beacon.location.longitude, + competition, + registration, + aircraft_beacon.altitude, + utc_to_local(aircraft_beacon.timestamp).strftime("%H:%M:%S"), + elapsed_seconds, + int(aircraft_beacon.track), + int(aircraft_beacon.ground_speed), + int(aircraft_beacon.climb_rate*10)/10, + aircraft_beacon.aircraft_type, + aircraft_beacon.receiver_name, + address, + code)) + + lines.append('') + xml = '\n'.join(lines) + + return xml diff --git a/ogn/backend/ognrange.py b/ogn/backend/ognrange.py new file mode 100644 index 0000000..48c86ed --- /dev/null +++ b/ogn/backend/ognrange.py @@ -0,0 +1,35 @@ +import json +from datetime import datetime, timedelta + +from sqlalchemy import func, case +from sqlalchemy.sql.expression import label +from ogn.model import Receiver + + +def alchemyencoder(obj): + import decimal + from datetime import datetime + """JSON encoder function for SQLAlchemy special classes.""" + if isinstance(obj, datetime): + return obj.strftime('%Y-%m-%d %H:%M') + elif isinstance(obj, decimal.Decimal): + return float(obj) + + +def stations2_filtered_pl(session): + last_10_minutes = datetime.utcnow() - timedelta(minutes=10) + + query = session.query( + Receiver.name.label('s'), + label('lt', func.round(func.ST_Y(Receiver.location_wkt)*10000)/10000), + label('lg', func.round(func.ST_X(Receiver.location_wkt)*10000)/10000), + case([(Receiver.lastseen > last_10_minutes, "U")], + else_="D").label('u'), + Receiver.lastseen.label('ut'), + label('v', Receiver.version + '.' + Receiver.platform)) \ + .order_by(Receiver.lastseen) + + res = session.execute(query) + stations = json.dumps({"stations": [dict(r) for r in res]}, default=alchemyencoder) + + return stations diff --git a/setup.py b/setup.py index 1c6ad32..79be068 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,8 @@ setup( 'dev': [ 'nose==1.3.7', 'coveralls==1.2', - 'flake8==3.5.0' + 'flake8==3.5.0', + 'xmlunittest==0.4.0' ] }, zip_safe=False diff --git a/tests/backend/test_backends.py b/tests/backend/test_backends.py new file mode 100644 index 0000000..d3ed434 --- /dev/null +++ b/tests/backend/test_backends.py @@ -0,0 +1,131 @@ +import unittest +from unittest import mock +import os +from datetime import datetime + +from xmlunittest import XmlTestMixin + +from ogn.model import Receiver, Device + +from ogn.backend.liveglidernet import rec, lxml +from ogn.backend.ognrange import stations2_filtered_pl + + +class TestDB(unittest.TestCase, XmlTestMixin): + session = None + engine = None + app = None + + def setUp(self): + os.environ['OGN_CONFIG_MODULE'] = 'config.test' + from ogn.commands.dbutils import engine, session + self.session = session + self.engine = engine + + from ogn.commands.database import init + init() + + # Prepare Beacons + self.r01 = Receiver(name='Koenigsdf', location_wkt='0101000020E610000061E8FED7A6EE26407F20661C10EA4740', lastseen='2017-12-20 10:00:00', altitude=601, version='0.2.5', platform='ARM') + self.r02 = Receiver(name='Bene', location_wkt='0101000020E6100000D5E76A2BF6C72640D4063A6DA0DB4740', lastseen='2017-12-20 09:45:00', altitude=609, version='0.2.7', platform='x64') + self.r03 = Receiver(name='Ohlstadt', location_wkt='0101000020E6100000057E678EBF772640A142883E32D44740', lastseen='2017-12-20 10:05:00', altitude=655, version='0.2.6', platform='ARM') + + self.d01 = Device(address='DD4711') + self.d02 = Device(address='DD0815') + + session.add(self.r01) + session.add(self.r02) + session.add(self.r03) + + session.add(self.d01) + session.add(self.d02) + session.commit() + + def tearDown(self): + session = self.session + session.execute("DELETE FROM receiver") + session.commit() + + @mock.patch('ogn.backend.liveglidernet.datetime') + def test_rec(self, datetime_mock): + session = self.session + + datetime_mock.utcnow.return_value = datetime(2017, 12, 20, 10, 0) + + data = rec(session).encode(encoding='utf-8') + + # Check the document + root = self.assertXmlDocument(data) + self.assertXmlNode(root, tag='markers') + self.assertXpathsOnlyOne(root, ('./m[@a="Koenigsdf"]', './m[@a="Bene"]', './m[@a="Ohlstadt"]')) + + expected = """ + + + + + + + """.encode(encoding='utf-8') + + # Check the complete document + self.assertXmlEquivalentOutputs(data, expected) + + print(data) + + @unittest.skip("not finished yet") + @mock.patch('ogn.backend.liveglidernet.datetime') + def test_lxml(self, datetime_mock): + session = self.session + + datetime_mock.utcnow.return_value = datetime(2017, 12, 20, 10, 0) + + data = lxml(session).encode(encoding='utf-8') + + # Check the document + root = self.assertXmlDocument(data) + self.assertXmlNode(root, tag='markers') + self.assertXpathsOnlyOne(root, ('./m[@a="Koenigsdf"]', './m[@a="Bene"]', './m[@a="Ohlstadt"]')) + + print(data) + + @mock.patch('ogn.backend.ognrange.datetime') + def test_stations2_filtered_pl(self, datetime_mock): + session = self.session + + datetime_mock.utcnow.return_value = datetime(2017, 12, 20, 10, 0) + + import json + + result = stations2_filtered_pl(session) + print(result) + + data = json.loads(result) + + stations = data["stations"] + self.assertEqual(len(stations), 3) + s1 = stations[0] + s2 = stations[1] + s3 = stations[2] + + self.assertEqual(s1["s"], 'Bene') + self.assertEqual(s1["lt"], 47.7158) + self.assertEqual(s1["lg"], 11.3906) + self.assertEqual(s1["u"], "D") # Down, because last beacon > 10min. ago + self.assertEqual(s1["ut"], "2017-12-20 09:45") + # self.assertEqual(s1["b"], 0) + self.assertEqual(s1["v"], "0.2.7.x64") + + self.assertEqual(s2["s"], 'Koenigsdf') + self.assertEqual(s2["lt"], 47.8286) + self.assertEqual(s2["lg"], 11.4661) + self.assertEqual(s2["u"], "U") + self.assertEqual(s2["ut"], "2017-12-20 10:00") + # self.assertEqual(s2["b"], 0) + self.assertEqual(s2["v"], "0.2.5.ARM") + + self.assertEqual(s3["s"], 'Ohlstadt') + + +if __name__ == '__main__': + unittest.main()