forked from platypush/platypush
parent
8351463a11
commit
2b595623b3
11 changed files with 538 additions and 197 deletions
|
@ -9,7 +9,6 @@ Backends
|
||||||
platypush/backend/button.flic.rst
|
platypush/backend/button.flic.rst
|
||||||
platypush/backend/camera.pi.rst
|
platypush/backend/camera.pi.rst
|
||||||
platypush/backend/chat.telegram.rst
|
platypush/backend/chat.telegram.rst
|
||||||
platypush/backend/gps.rst
|
|
||||||
platypush/backend/http.rst
|
platypush/backend/http.rst
|
||||||
platypush/backend/midi.rst
|
platypush/backend/midi.rst
|
||||||
platypush/backend/music.mopidy.rst
|
platypush/backend/music.mopidy.rst
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
``gps``
|
|
||||||
=========================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.gps
|
|
||||||
:members:
|
|
||||||
|
|
5
docs/source/platypush/plugins/gps.rst
Normal file
5
docs/source/platypush/plugins/gps.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``gps``
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.gps
|
||||||
|
:members:
|
|
@ -46,6 +46,7 @@ Plugins
|
||||||
platypush/plugins/gotify.rst
|
platypush/plugins/gotify.rst
|
||||||
platypush/plugins/gpio.rst
|
platypush/plugins/gpio.rst
|
||||||
platypush/plugins/gpio.zeroborg.rst
|
platypush/plugins/gpio.zeroborg.rst
|
||||||
|
platypush/plugins/gps.rst
|
||||||
platypush/plugins/graphite.rst
|
platypush/plugins/graphite.rst
|
||||||
platypush/plugins/hid.rst
|
platypush/plugins/hid.rst
|
||||||
platypush/plugins/http.request.rst
|
platypush/plugins/http.request.rst
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
from platypush.backend import Backend
|
|
||||||
from platypush.message.event.gps import GPSVersionEvent, GPSDeviceEvent, GPSUpdateEvent
|
|
||||||
|
|
||||||
|
|
||||||
class GpsBackend(Backend):
|
|
||||||
"""
|
|
||||||
This backend can interact with a GPS device and listen for events.
|
|
||||||
|
|
||||||
Once installed gpsd you need to run it and associate it to your device. Example if your GPS device communicates
|
|
||||||
over USB and is available on /dev/ttyUSB0::
|
|
||||||
|
|
||||||
[sudo] gpsd /dev/ttyUSB0 -F /var/run/gpsd.sock
|
|
||||||
|
|
||||||
The best option is probably to run gpsd at startup as a systemd service.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_fail_sleep_time = 5.0
|
|
||||||
_lat_lng_tolerance = 1e-5
|
|
||||||
_alt_tolerance = 0.5
|
|
||||||
|
|
||||||
def __init__(self, gpsd_server='localhost', gpsd_port=2947, **kwargs):
|
|
||||||
"""
|
|
||||||
:param gpsd_server: gpsd daemon server name/address (default: localhost)
|
|
||||||
:type gpsd_server: str
|
|
||||||
:param gpsd_port: Port of the gpsd daemon (default: 2947)
|
|
||||||
:type gpsd_port: int or str
|
|
||||||
"""
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
|
|
||||||
self.gpsd_server = gpsd_server
|
|
||||||
self.gpsd_port = gpsd_port
|
|
||||||
self._session = None
|
|
||||||
self._session_lock = threading.RLock()
|
|
||||||
self._devices = {}
|
|
||||||
|
|
||||||
def _get_session(self):
|
|
||||||
import gps
|
|
||||||
|
|
||||||
with self._session_lock:
|
|
||||||
if not self._session:
|
|
||||||
self._session = gps.gps(
|
|
||||||
host=self.gpsd_server, port=self.gpsd_port, reconnect=True
|
|
||||||
)
|
|
||||||
self._session.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)
|
|
||||||
|
|
||||||
return self._session
|
|
||||||
|
|
||||||
def _gps_report_to_event(self, report):
|
|
||||||
if report.get('class').lower() == 'version':
|
|
||||||
return GPSVersionEvent(
|
|
||||||
release=report.get('release'),
|
|
||||||
rev=report.get('rev'),
|
|
||||||
proto_major=report.get('proto_major'),
|
|
||||||
proto_minor=report.get('proto_minor'),
|
|
||||||
)
|
|
||||||
if report.get('class').lower() == 'devices':
|
|
||||||
for device in report.get('devices', []):
|
|
||||||
if device.get(
|
|
||||||
'path'
|
|
||||||
) not in self._devices or device != self._devices.get('path'):
|
|
||||||
# noinspection DuplicatedCode
|
|
||||||
self._devices[device.get('path')] = device
|
|
||||||
return GPSDeviceEvent(
|
|
||||||
path=device.get('path'),
|
|
||||||
activated=device.get('activated'),
|
|
||||||
native=device.get('native'),
|
|
||||||
bps=device.get('bps'),
|
|
||||||
parity=device.get('parity'),
|
|
||||||
stopbits=device.get('stopbits'),
|
|
||||||
cycle=device.get('cycle'),
|
|
||||||
driver=device.get('driver'),
|
|
||||||
)
|
|
||||||
if report.get('class').lower() == 'device':
|
|
||||||
# noinspection DuplicatedCode
|
|
||||||
self._devices[report.get('path')] = report
|
|
||||||
return GPSDeviceEvent(
|
|
||||||
path=report.get('path'),
|
|
||||||
activated=report.get('activated'),
|
|
||||||
native=report.get('native'),
|
|
||||||
bps=report.get('bps'),
|
|
||||||
parity=report.get('parity'),
|
|
||||||
stopbits=report.get('stopbits'),
|
|
||||||
cycle=report.get('cycle'),
|
|
||||||
driver=report.get('driver'),
|
|
||||||
)
|
|
||||||
if report.get('class').lower() == 'tpv':
|
|
||||||
return GPSUpdateEvent(
|
|
||||||
device=report.get('device'),
|
|
||||||
latitude=report.get('lat'),
|
|
||||||
longitude=report.get('lon'),
|
|
||||||
altitude=report.get('alt'),
|
|
||||||
mode=report.get('mode'),
|
|
||||||
epv=report.get('epv'),
|
|
||||||
eph=report.get('eph'),
|
|
||||||
sep=report.get('sep'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
super().run()
|
|
||||||
self.logger.info(
|
|
||||||
'Initialized GPS backend on {}:{}'.format(self.gpsd_server, self.gpsd_port)
|
|
||||||
)
|
|
||||||
last_event = None
|
|
||||||
|
|
||||||
while not self.should_stop():
|
|
||||||
try:
|
|
||||||
session = self._get_session()
|
|
||||||
report = session.next()
|
|
||||||
event = self._gps_report_to_event(report)
|
|
||||||
if event and (
|
|
||||||
last_event is None
|
|
||||||
or abs(
|
|
||||||
(last_event.args.get('latitude') or 0)
|
|
||||||
- (event.args.get('latitude') or 0)
|
|
||||||
)
|
|
||||||
>= self._lat_lng_tolerance
|
|
||||||
or abs(
|
|
||||||
(last_event.args.get('longitude') or 0)
|
|
||||||
- (event.args.get('longitude') or 0)
|
|
||||||
)
|
|
||||||
>= self._lat_lng_tolerance
|
|
||||||
or abs(
|
|
||||||
(last_event.args.get('altitude') or 0)
|
|
||||||
- (event.args.get('altitude') or 0)
|
|
||||||
)
|
|
||||||
>= self._alt_tolerance
|
|
||||||
):
|
|
||||||
self.bus.post(event)
|
|
||||||
last_event = event
|
|
||||||
except Exception as e:
|
|
||||||
if isinstance(e, StopIteration):
|
|
||||||
self.logger.warning(
|
|
||||||
'GPS service connection lost, check that gpsd is running'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.logger.exception(e)
|
|
||||||
|
|
||||||
self._session = None
|
|
||||||
time.sleep(self._fail_sleep_time)
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -1,20 +0,0 @@
|
||||||
manifest:
|
|
||||||
events:
|
|
||||||
platypush.message.event.gps.GPSDeviceEvent: when a GPS device is connected or
|
|
||||||
updated
|
|
||||||
platypush.message.event.gps.GPSUpdateEvent: when a GPS device has new data
|
|
||||||
platypush.message.event.gps.GPSVersionEvent: when a GPS device advertises its
|
|
||||||
version data
|
|
||||||
install:
|
|
||||||
apk:
|
|
||||||
- gpsd
|
|
||||||
apt:
|
|
||||||
- gpsd
|
|
||||||
dnf:
|
|
||||||
- gpsd
|
|
||||||
pacman:
|
|
||||||
- gpsd
|
|
||||||
pip:
|
|
||||||
- gps
|
|
||||||
package: platypush.backend.gps
|
|
||||||
type: backend
|
|
|
@ -1,44 +1,97 @@
|
||||||
|
from abc import ABC
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from platypush.message.event import Event
|
from platypush.message.event import Event
|
||||||
|
|
||||||
|
|
||||||
class GPSEvent(Event):
|
class GPSEvent(Event, ABC):
|
||||||
"""
|
"""
|
||||||
Generic class for GPS events
|
Generic class for GPS events.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class GPSVersionEvent(GPSEvent):
|
|
||||||
"""
|
|
||||||
Event usually triggered on startup or reconnection, when the GPS device advertises its version parameters
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, release=None, rev=None, proto_major=None, proto_minor=None, *args, **kwargs):
|
|
||||||
super().__init__(release=release, rev=rev, proto_major=proto_major, proto_minor=proto_minor, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class GPSDeviceEvent(GPSEvent):
|
class GPSDeviceEvent(GPSEvent):
|
||||||
"""
|
"""
|
||||||
Event triggered when a new GPS device is connected or reconfigured
|
Event triggered when a new GPS device is connected or reconfigured.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path, activated=None, native=False, bps=None, parity=None, stopbits=None,
|
def __init__(
|
||||||
cycle=None, driver=None, *args, **kwargs):
|
self,
|
||||||
super().__init__(*args, path=path, activated=activated, native=native, bps=bps, parity=parity,
|
path: str,
|
||||||
stopbits=stopbits, cycle=cycle, driver=driver, **kwargs)
|
*args,
|
||||||
|
activated: Optional[datetime] = None,
|
||||||
|
native: bool = False,
|
||||||
|
baudrate: Optional[int] = None,
|
||||||
|
parity: Optional[str] = None,
|
||||||
|
stopbits: Optional[int] = None,
|
||||||
|
cycle: Optional[float] = None,
|
||||||
|
driver: Optional[str] = None,
|
||||||
|
subtype: Optional[str] = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param path: Device path.
|
||||||
|
:param activated: Device activation timestamp.
|
||||||
|
:param native: Device native status.
|
||||||
|
:param baudrate: Device baudrate.
|
||||||
|
:param parity: Device parity.
|
||||||
|
:param stopbits: Device stopbits.
|
||||||
|
:param cycle: Device cycle.
|
||||||
|
:param driver: Device driver.
|
||||||
|
:param subtype: Device subtype.
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
*args,
|
||||||
|
path=path,
|
||||||
|
activated=activated,
|
||||||
|
native=native,
|
||||||
|
baudrate=baudrate,
|
||||||
|
parity=parity,
|
||||||
|
stopbits=stopbits,
|
||||||
|
cycle=cycle,
|
||||||
|
driver=driver,
|
||||||
|
subtype=subtype,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GPSUpdateEvent(GPSEvent):
|
class GPSLocationUpdateEvent(GPSEvent):
|
||||||
"""
|
"""
|
||||||
Event triggered upon GPS status update
|
Event triggered upon GPS status update.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, device=None, latitude=None, longitude=None, altitude=None, mode=None, epv=None, eph=None,
|
def __init__(
|
||||||
sep=None, *args, **kwargs):
|
self,
|
||||||
super().__init__(*args, device=device, latitude=latitude, longitude=longitude, altitude=altitude,
|
*args,
|
||||||
mode=mode, epv=epv, eph=eph, sep=sep, **kwargs)
|
device=None,
|
||||||
|
latitude=None,
|
||||||
|
longitude=None,
|
||||||
|
altitude=None,
|
||||||
|
mode=None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
*args,
|
||||||
|
device=device,
|
||||||
|
latitude=latitude,
|
||||||
|
longitude=longitude,
|
||||||
|
altitude=altitude,
|
||||||
|
mode=mode,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GPSEnabledEvent(GPSEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when the GPS polling is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class GPSDisabledEvent(GPSEvent):
|
||||||
|
"""
|
||||||
|
Event triggered when the GPS polling is disabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
216
platypush/plugins/gps/__init__.py
Normal file
216
platypush/plugins/gps/__init__.py
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
import threading
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import asdict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from platypush.plugins import RunnablePlugin, action
|
||||||
|
from platypush.message.event.gps import (
|
||||||
|
GPSDeviceEvent,
|
||||||
|
GPSDisabledEvent,
|
||||||
|
GPSEnabledEvent,
|
||||||
|
GPSLocationUpdateEvent,
|
||||||
|
)
|
||||||
|
from platypush.schemas.gps import GpsDeviceSchema, GpsStatusSchema
|
||||||
|
|
||||||
|
from ._model import DeviceMode, GpsDevice, GpsStatus
|
||||||
|
|
||||||
|
|
||||||
|
class GpsPlugin(RunnablePlugin):
|
||||||
|
"""
|
||||||
|
This plugin can interact with a GPS device compatible with `gpsd
|
||||||
|
<https://gpsd.io/>`_ and emit events when the location changes.
|
||||||
|
|
||||||
|
It requires ``gpsd`` to run on a system with a compatible GPS device
|
||||||
|
connected - most of the off-the-shelf GPS devices over USB or serial
|
||||||
|
interfaces should tick the box.
|
||||||
|
|
||||||
|
For example, if your GPS device communicates over USB and is available on
|
||||||
|
/dev/ttyUSB0, you can start the gpsd daemon with the following command
|
||||||
|
before starting Platypush::
|
||||||
|
|
||||||
|
[sudo] gpsd /dev/ttyUSB0 [-S 2947]
|
||||||
|
|
||||||
|
It will expose GPS events over the port ``2947`` by default, and you can
|
||||||
|
subscribe to them through this plugin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_default_gpsd_port = 2947
|
||||||
|
_default_poll_interval = 5.0
|
||||||
|
_lat_lng_tolerance = 1e-5
|
||||||
|
_alt_tolerance = 0.5
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
gpsd_server: str = 'localhost',
|
||||||
|
gpsd_port: int = _default_gpsd_port,
|
||||||
|
poll_interval: float = _default_poll_interval,
|
||||||
|
enable_on_start: bool = True,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param gpsd_server: gpsd daemon server name/address (default: localhost).
|
||||||
|
:param gpsd_port: Port of the gpsd daemon (default: 2947).
|
||||||
|
:param poll_interval: How long to wait before polling the GPS device
|
||||||
|
again in case of error (default: 5 seconds).
|
||||||
|
:param enable_on_start: If True, the GPS polling will be enabled when the
|
||||||
|
plugin starts (default: True). Otherwise, it'll have to be enabled by
|
||||||
|
calling the :meth:`.enable` action.
|
||||||
|
"""
|
||||||
|
super().__init__(poll_interval=poll_interval, **kwargs)
|
||||||
|
|
||||||
|
self.gpsd_server = gpsd_server
|
||||||
|
self.gpsd_port = gpsd_port
|
||||||
|
self._enable_on_start = enable_on_start
|
||||||
|
self._session = None
|
||||||
|
self._session_lock = threading.RLock()
|
||||||
|
self._status = GpsStatus()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _get_session(self):
|
||||||
|
import gps
|
||||||
|
|
||||||
|
with self._session_lock:
|
||||||
|
if not self._session:
|
||||||
|
self._session = gps.gps(
|
||||||
|
host=self.gpsd_server, port=self.gpsd_port, reconnect=True # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self._session
|
||||||
|
|
||||||
|
with self._session_lock:
|
||||||
|
try:
|
||||||
|
self.disable()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning('Error disabling GPSD watch: %s', e)
|
||||||
|
|
||||||
|
self._session.close()
|
||||||
|
self._session = None
|
||||||
|
|
||||||
|
def _update_device(self, device: dict):
|
||||||
|
path = device.get('path')
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
cur_dev = self._status.devices.get(path)
|
||||||
|
new_dev = GpsDevice(**GpsDeviceSchema().load(device)) # type: ignore
|
||||||
|
if cur_dev and asdict(cur_dev) == asdict(new_dev):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._status.devices[path] = new_dev
|
||||||
|
self._bus.post(GPSDeviceEvent(**asdict(new_dev)))
|
||||||
|
|
||||||
|
def _handle_location_update(self, report: dict):
|
||||||
|
dev, lat, long, alt, mode, t = (
|
||||||
|
report.get('device'),
|
||||||
|
report.get('lat'),
|
||||||
|
report.get('lon'),
|
||||||
|
report.get('alt'),
|
||||||
|
report.get('mode'),
|
||||||
|
report.get('time'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (dev and lat and long and mode):
|
||||||
|
return
|
||||||
|
|
||||||
|
dev_mode = DeviceMode(mode)
|
||||||
|
self._status.timestamp = datetime.fromisoformat(t) if t else None
|
||||||
|
self._status.devices[dev].mode = dev_mode
|
||||||
|
|
||||||
|
if not (
|
||||||
|
abs((self._status.latitude or 0) - lat) >= self._lat_lng_tolerance
|
||||||
|
or abs((self._status.longitude or 0) - long) >= self._lat_lng_tolerance
|
||||||
|
or abs((self._status.altitude or 0) - (alt or 0)) >= self._alt_tolerance
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
event = GPSLocationUpdateEvent(
|
||||||
|
device=dev,
|
||||||
|
latitude=lat,
|
||||||
|
longitude=long,
|
||||||
|
altitude=alt,
|
||||||
|
mode=dev_mode.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._status.latitude = lat
|
||||||
|
self._status.longitude = long
|
||||||
|
self._status.altitude = alt
|
||||||
|
self._bus.post(event)
|
||||||
|
|
||||||
|
def _handle_report(self, report: dict):
|
||||||
|
cls = report['class'].lower()
|
||||||
|
if cls == 'version':
|
||||||
|
self.logger.info('Received GPSD version event: %s', dict(report))
|
||||||
|
return
|
||||||
|
|
||||||
|
if cls == 'watch':
|
||||||
|
evt_type = GPSEnabledEvent if report.get('enable') else GPSDisabledEvent
|
||||||
|
self._bus.post(evt_type())
|
||||||
|
return
|
||||||
|
|
||||||
|
if cls == 'devices':
|
||||||
|
for device in report.get('devices', []):
|
||||||
|
self._update_device(device)
|
||||||
|
return
|
||||||
|
|
||||||
|
if cls == 'device':
|
||||||
|
self._update_device(report)
|
||||||
|
return
|
||||||
|
|
||||||
|
if cls == 'tpv':
|
||||||
|
self._handle_location_update(report)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def enable(self):
|
||||||
|
"""
|
||||||
|
Enable the GPS polling.
|
||||||
|
"""
|
||||||
|
import gps
|
||||||
|
|
||||||
|
assert self._session, 'GPSD session not initialized'
|
||||||
|
self._session.stream(gps.WATCH_ENABLE | gps.WATCH_NEWSTYLE)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def disable(self):
|
||||||
|
"""
|
||||||
|
Disable the GPS polling.
|
||||||
|
"""
|
||||||
|
import gps
|
||||||
|
|
||||||
|
assert self._session, 'GPSD session not initialized'
|
||||||
|
self._session.stream(gps.WATCH_DISABLE)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def status(self):
|
||||||
|
"""
|
||||||
|
:returns: The current GPS status:
|
||||||
|
|
||||||
|
.. schema:: gps.GpsStatusSchema
|
||||||
|
|
||||||
|
"""
|
||||||
|
return GpsStatusSchema().dump(self._status)
|
||||||
|
|
||||||
|
def main(self):
|
||||||
|
while not self.should_stop():
|
||||||
|
first_run = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
with self._get_session() as session:
|
||||||
|
if first_run and self._enable_on_start:
|
||||||
|
self.enable()
|
||||||
|
first_run = False
|
||||||
|
|
||||||
|
while not self.should_stop():
|
||||||
|
report: dict = session.next() # type: ignore
|
||||||
|
self._handle_report(report)
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, StopIteration):
|
||||||
|
self.logger.warning(
|
||||||
|
'GPS service connection lost, check that gpsd is running'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.logger.exception(e)
|
||||||
|
|
||||||
|
self.wait_stop(self.poll_interval)
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
47
platypush/plugins/gps/_model.py
Normal file
47
platypush/plugins/gps/_model.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceMode(Enum):
|
||||||
|
"""
|
||||||
|
GPS device mode.
|
||||||
|
"""
|
||||||
|
|
||||||
|
NO_FIX = 1 # No fix
|
||||||
|
TWO_D = 2 # 2D fix
|
||||||
|
THREE_D = 3 # 3D fix (including altitude)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GpsDevice:
|
||||||
|
"""
|
||||||
|
Models the GPS device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
path: str
|
||||||
|
activated: Optional[datetime] = None
|
||||||
|
native: bool = False
|
||||||
|
baudrate: Optional[int] = None
|
||||||
|
parity: str = 'N'
|
||||||
|
stopbits: Optional[int] = None
|
||||||
|
cycle: Optional[float] = None
|
||||||
|
driver: Optional[str] = None
|
||||||
|
subtype: Optional[str] = None
|
||||||
|
mode: Optional[DeviceMode] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GpsStatus:
|
||||||
|
"""
|
||||||
|
Models the status of the GPS service.
|
||||||
|
"""
|
||||||
|
|
||||||
|
latitude: Optional[float] = None
|
||||||
|
longitude: Optional[float] = None
|
||||||
|
altitude: Optional[float] = None
|
||||||
|
speed: Optional[float] = None
|
||||||
|
satellites_used: int = 0
|
||||||
|
devices: Dict[str, GpsDevice] = field(default_factory=dict)
|
||||||
|
timestamp: Optional[datetime] = None
|
22
platypush/plugins/gps/manifest.yaml
Normal file
22
platypush/plugins/gps/manifest.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
manifest:
|
||||||
|
events:
|
||||||
|
- platypush.message.event.gps.GPSDeviceEvent
|
||||||
|
- platypush.message.event.gps.GPSDisabledEvent
|
||||||
|
- platypush.message.event.gps.GPSEnabledEvent
|
||||||
|
- platypush.message.event.gps.GPSLocationUpdateEvent
|
||||||
|
install:
|
||||||
|
apk:
|
||||||
|
- gpsd
|
||||||
|
- py3-gpsd
|
||||||
|
apt:
|
||||||
|
- gpsd
|
||||||
|
- python3-gps
|
||||||
|
dnf:
|
||||||
|
- gpsd
|
||||||
|
- python-gpsd
|
||||||
|
pacman:
|
||||||
|
- gpsd
|
||||||
|
pip:
|
||||||
|
- gps
|
||||||
|
package: platypush.plugins.gps
|
||||||
|
type: plugin
|
169
platypush/schemas/gps.py
Normal file
169
platypush/schemas/gps.py
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
from marshmallow import EXCLUDE, fields, pre_load
|
||||||
|
from marshmallow.schema import Schema
|
||||||
|
|
||||||
|
from platypush.schemas import DateTime
|
||||||
|
|
||||||
|
|
||||||
|
class GpsDeviceSchema(Schema):
|
||||||
|
"""
|
||||||
|
Schema for a GPS device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
class Meta: # type: ignore
|
||||||
|
"""
|
||||||
|
Exclude unknown fields from the deserialized output.
|
||||||
|
"""
|
||||||
|
|
||||||
|
unknown = EXCLUDE
|
||||||
|
|
||||||
|
path = fields.String(
|
||||||
|
required=True,
|
||||||
|
metadata={
|
||||||
|
"description": "Device path",
|
||||||
|
"example": "/dev/ttyUSB0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
activated = DateTime(
|
||||||
|
metadata={
|
||||||
|
"description": "Device activation status",
|
||||||
|
"example": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
native = fields.Boolean(
|
||||||
|
metadata={
|
||||||
|
"description": "Device native status",
|
||||||
|
"example": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
baudrate = fields.Integer(
|
||||||
|
data_key="bps",
|
||||||
|
metadata={
|
||||||
|
"description": "Device baudrate",
|
||||||
|
"example": 9600,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
parity = fields.String(
|
||||||
|
metadata={
|
||||||
|
"description": "Device parity",
|
||||||
|
"example": "N",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
stopbits = fields.Integer(
|
||||||
|
metadata={
|
||||||
|
"description": "Device stopbits",
|
||||||
|
"example": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
cycle = fields.Integer(
|
||||||
|
metadata={
|
||||||
|
"description": "Device cycle",
|
||||||
|
"example": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
driver = fields.String(
|
||||||
|
metadata={
|
||||||
|
"description": "Device driver",
|
||||||
|
"example": "NMEA",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
subtype = fields.String(
|
||||||
|
metadata={
|
||||||
|
"description": "Device subtype",
|
||||||
|
"example": "AXN_2.31_3339_13101700,5632,PA6H,1.0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
mode = fields.String(
|
||||||
|
validate=lambda mode: mode in ["NO_FIX", "TWO_D", "THREE_D"],
|
||||||
|
metadata={
|
||||||
|
"description": "Device mode, one of NO_FIX, TWO_D, THREE_D",
|
||||||
|
"example": "3D",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@pre_load
|
||||||
|
def pre_load(self, data, **_):
|
||||||
|
from platypush.plugins.gps import DeviceMode
|
||||||
|
|
||||||
|
if data and data.get("mode"):
|
||||||
|
data["mode"] = DeviceMode(data["mode"]).value
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class GpsStatusSchema(Schema):
|
||||||
|
"""
|
||||||
|
Schema for the GPS status.
|
||||||
|
"""
|
||||||
|
|
||||||
|
latitude = fields.Float(
|
||||||
|
metadata={
|
||||||
|
"description": "Latitude",
|
||||||
|
"example": 45.4642,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
longitude = fields.Float(
|
||||||
|
metadata={
|
||||||
|
"description": "Longitude",
|
||||||
|
"example": 9.1900,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
altitude = fields.Float(
|
||||||
|
metadata={
|
||||||
|
"description": "Altitude (in meters)",
|
||||||
|
"example": 100,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
speed = fields.Float(
|
||||||
|
metadata={
|
||||||
|
"description": "Measured speed, if available (in km/h)",
|
||||||
|
"example": 10,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
satellites_used = fields.Integer(
|
||||||
|
metadata={
|
||||||
|
"description": "Number of satellites used for the fix",
|
||||||
|
"example": 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
timestamp = DateTime(
|
||||||
|
metadata={
|
||||||
|
"description": "Timestamp of the last GPS update",
|
||||||
|
"example": "2021-08-01T00:00:00",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
devices = fields.Dict(
|
||||||
|
keys=fields.String(),
|
||||||
|
values=fields.Nested(GpsDeviceSchema),
|
||||||
|
metadata={
|
||||||
|
"description": "Available GPS devices",
|
||||||
|
"example": {
|
||||||
|
"/dev/ttyUSB0": {
|
||||||
|
"path": "/dev/ttyUSB0",
|
||||||
|
"activated": "2021-08-01T00:00:00",
|
||||||
|
"native": False,
|
||||||
|
"baudrate": 9600,
|
||||||
|
"parity": "N",
|
||||||
|
"stopbits": 1,
|
||||||
|
"cycle": 1,
|
||||||
|
"driver": "NMEA",
|
||||||
|
"subtype": "AXN_2.31_3339_13101700,5632,PA6H,1.0",
|
||||||
|
"mode": "3D",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
Loading…
Reference in a new issue