Added sun plugin [closes #194]

This commit is contained in:
Fabio Manganiello 2021-07-22 01:02:15 +02:00
parent 47228f2432
commit 371dd6da0a
13 changed files with 248 additions and 13 deletions

View file

@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2. Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
## [Unreleased]
### Added
- Added `sun` plugin for sunrise/sunset events.
## [0.21.2] - 2021-07-20 ## [0.21.2] - 2021-07-20
### Added ### Added

View file

@ -54,6 +54,7 @@ Events
platypush/events/serial.rst platypush/events/serial.rst
platypush/events/sound.rst platypush/events/sound.rst
platypush/events/stt.rst platypush/events/stt.rst
platypush/events/sun.rst
platypush/events/tensorflow.rst platypush/events/tensorflow.rst
platypush/events/todoist.rst platypush/events/todoist.rst
platypush/events/torrent.rst platypush/events/torrent.rst

View file

@ -0,0 +1,5 @@
``platypush.message.event.sun``
===============================
.. automodule:: platypush.message.event.sun
:members:

View file

@ -0,0 +1,5 @@
``sun``
=======
.. automodule:: platypush.plugins.sun
:members:

View file

@ -119,6 +119,7 @@ Plugins
platypush/plugins/stt.deepspeech.rst platypush/plugins/stt.deepspeech.rst
platypush/plugins/stt.picovoice.hotword.rst platypush/plugins/stt.picovoice.hotword.rst
platypush/plugins/stt.picovoice.speech.rst platypush/plugins/stt.picovoice.speech.rst
platypush/plugins/sun.rst
platypush/plugins/switch.rst platypush/plugins/switch.rst
platypush/plugins/switch.tplink.rst platypush/plugins/switch.tplink.rst
platypush/plugins/switch.wemo.rst platypush/plugins/switch.wemo.rst

View file

@ -12,7 +12,7 @@ import sys
from .bus.redis import RedisBus from .bus.redis import RedisBus
from .config import Config from .config import Config
from .context import register_backends from .context import register_backends, register_plugins
from .cron.scheduler import CronScheduler from .cron.scheduler import CronScheduler
from .event.processor import EventProcessor from .event.processor import EventProcessor
from .logger import Logger from .logger import Logger
@ -20,8 +20,7 @@ from .message.event import Event
from .message.event.application import ApplicationStartedEvent from .message.event.application import ApplicationStartedEvent
from .message.request import Request from .message.request import Request
from .message.response import Response from .message.response import Response
from .utils import set_thread_name from .utils import set_thread_name, get_enabled_plugins
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>' __author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
__version__ = '0.21.2' __version__ = '0.21.2'
@ -160,9 +159,15 @@ class Daemon:
def stop_app(self): def stop_app(self):
""" Stops the backends and the bus """ """ Stops the backends and the bus """
from .plugins import RunnablePlugin
for backend in self.backends.values(): for backend in self.backends.values():
backend.stop() backend.stop()
for plugin in get_enabled_plugins().values():
if isinstance(plugin, RunnablePlugin):
plugin.stop()
self.bus.stop() self.bus.stop()
if self.cron_scheduler: if self.cron_scheduler:
self.cron_scheduler.stop() self.cron_scheduler.stop()
@ -184,6 +189,9 @@ class Daemon:
for backend in self.backends.values(): for backend in self.backends.values():
backend.start() backend.start()
# Initialize the plugins
register_plugins(bus=self.bus)
# Start the cron scheduler # Start the cron scheduler
if Config.get_cronjobs(): if Config.get_cronjobs():
self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs()) self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs())

View file

@ -5,6 +5,7 @@ import logging
from threading import RLock from threading import RLock
from ..config import Config from ..config import Config
from ..utils import get_enabled_plugins
logger = logging.getLogger('platypush:context') logger = logging.getLogger('platypush:context')
@ -21,6 +22,7 @@ plugins_init_locks = {}
# Reference to the main application bus # Reference to the main application bus
main_bus = None main_bus = None
def register_backends(bus=None, global_scope=False, **kwargs): def register_backends(bus=None, global_scope=False, **kwargs):
""" Initialize the backend objects based on the configuration and returns """ Initialize the backend objects based on the configuration and returns
a name -> backend_instance map. a name -> backend_instance map.
@ -59,6 +61,16 @@ def register_backends(bus=None, global_scope=False, **kwargs):
return backends return backends
def register_plugins(bus=None):
from ..plugins import RunnablePlugin
for plugin in get_enabled_plugins().values():
if isinstance(plugin, RunnablePlugin):
plugin.bus = bus
plugin.start()
def get_backend(name): def get_backend(name):
""" Returns the backend instance identified by name if it exists """ """ Returns the backend instance identified by name if it exists """

View file

@ -0,0 +1,36 @@
from datetime import datetime
from typing import Optional
from platypush.message.event import Event
class SunEvent(Event):
"""
Base class for sun related events (sunrise and sunset).
"""
def __init__(self, latitude: Optional[float] = None, longitude: Optional[float] = None,
time: Optional[datetime] = None, *args, **kwargs):
"""
:param latitude: Latitude for the sun event.
:param longitude: Longitude for the sun event.
:param time: Event timestamp.
"""
super().__init__(*args, latitude=latitude, longitude=longitude, time=time, **kwargs)
self.latitude = latitude
self.longitude = longitude
self.time = time
class SunriseEvent(SunEvent):
"""
Class for sunrise events.
"""
class SunsetEvent(SunEvent):
"""
Class for sunset events.
"""
# vim:sw=4:ts=4:et:

View file

@ -1,10 +1,15 @@
import logging import logging
import threading
import time
from abc import ABC
from functools import wraps from functools import wraps
from typing import Optional
from platypush.bus import Bus
from platypush.event import EventGenerator from platypush.event import EventGenerator
from platypush.message.response import Response from platypush.message.response import Response
from platypush.utils import get_decorators, get_plugin_name_by_class from platypush.utils import get_decorators, get_plugin_name_by_class, set_thread_name
def action(f): def action(f):
@ -53,4 +58,56 @@ class Plugin(EventGenerator):
return getattr(self, method)(*args, **kwargs) return getattr(self, method)(*args, **kwargs)
class RunnablePlugin(ABC, Plugin):
"""
Class for runnable plugins - i.e. plugins that have a start/stop method and can be started.
"""
def __init__(self, poll_interval: Optional[float] = None, **kwargs):
"""
:param poll_interval: How often the :meth:`.loop` function should be execute (default: None, no pause/interval).
"""
super().__init__(**kwargs)
self.poll_interval = poll_interval
self.bus: Optional[Bus] = None
self._should_stop = threading.Event()
self._thread: Optional[threading.Thread] = None
def main(self):
raise NotImplementedError()
def should_stop(self):
return self._should_stop.is_set()
def start(self):
set_thread_name(self.__class__.__name__)
self._thread = threading.Thread(target=self._runner)
self._thread.start()
def stop(self):
self._should_stop.set()
if self._thread and self._thread.is_alive():
self.logger.info(f'Waiting for {self.__class__.__name__} to stop')
# noinspection PyBroadException
try:
self._thread.join()
except:
pass
self.logger.info(f'{self.__class__.__name__} stopped')
def _runner(self):
self.logger.info(f'Starting {self.__class__.__name__}')
while not self.should_stop():
try:
self.main()
except Exception as e:
self.logger.exception(e)
if self.poll_interval:
time.sleep(self.poll_interval)
self._thread = None
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

87
platypush/plugins/sun.py Normal file
View file

@ -0,0 +1,87 @@
import datetime
import time
from typing import Optional
import requests
from dateutil.tz import gettz, tzutc
from platypush.message.event.sun import SunriseEvent, SunsetEvent
from platypush.plugins import RunnablePlugin, action
from platypush.schemas.sun import SunEventsSchema
class SunPlugin(RunnablePlugin):
"""
Plugin to get sunset/sunrise events and info for a certain location.
Triggers:
* :class:`platypush.message.event.sun.SunriseEvent` on sunrise.
* :class:`platypush.message.event.sun.SunsetEvent` on sunset.
"""
_base_url = 'https://api.sunrise-sunset.org/json'
_attr_to_event_class = {
'sunrise': SunriseEvent,
'sunset': SunsetEvent,
}
def __init__(self, latitude: float, longitude: float, **kwargs):
"""
:param latitude: Default latitude.
:param longitude: Default longitude.
"""
super().__init__(**kwargs)
self.latitude = latitude
self.longitude = longitude
def main(self):
while not self.should_stop():
# noinspection PyUnresolvedReferences
next_events = self.get_events().output
next_events = sorted([
event_class(latitude=self.latitude, longitude=self.longitude, time=next_events[attr])
for attr, event_class in self._attr_to_event_class.items()
if next_events.get(attr)
], key=lambda t: t.time)
for event in next_events:
# noinspection PyTypeChecker
dt = datetime.datetime.fromisoformat(event.time)
while (not self.should_stop()) and (dt > datetime.datetime.now(tz=gettz())):
time.sleep(1)
if dt <= datetime.datetime.now(tz=gettz()):
self.bus.post(event)
@staticmethod
def _convert_time(t: str) -> datetime.datetime:
now = datetime.datetime.now().replace(tzinfo=gettz()) # lgtm [py/call-to-non-callable]
dt = datetime.datetime.strptime(t, '%H:%M:%S %p')
dt = datetime.datetime(year=now.year, month=now.month, day=now.day,
hour=dt.hour, minute=dt.minute, second=dt.second, tzinfo=tzutc())
if dt < now:
dt += datetime.timedelta(days=1)
return datetime.datetime.fromtimestamp(dt.timestamp(), tz=gettz())
@action
def get_events(self, latitude: Optional[float] = None, longitude: Optional[float] = None) -> dict:
"""
Return the next sun events.
:param latitude: Default latitude override.
:param longitude: Default longitude override.
:return: .. schema:: sun.SunEventsSchema
"""
response = requests.get(self._base_url, params={
'lat': latitude or self.latitude,
'lng': longitude or self.longitude,
}).json().get('results', {})
schema = SunEventsSchema()
return schema.dump({
attr: self._convert_time(t)
for attr, t in response.items()
if attr in schema.declared_fields.keys()
})

View file

@ -0,0 +1,10 @@
from datetime import datetime
from typing import Optional
def normalize_datetime(dt: str) -> Optional[datetime]:
if not dt:
return
if dt.endswith('Z'):
dt = dt[:-1] + '+00:00'
return datetime.fromisoformat(dt)

View file

@ -1,11 +1,12 @@
from datetime import datetime from datetime import datetime
from typing import Union, Optional from typing import Union
from marshmallow import fields, pre_dump from marshmallow import fields, pre_dump
from marshmallow.schema import Schema from marshmallow.schema import Schema
from marshmallow.validate import OneOf, Range from marshmallow.validate import OneOf, Range
from platypush.plugins.media import PlayerState from platypush.plugins.media import PlayerState
from platypush.schemas import normalize_datetime
device_types = [ device_types = [
'Unknown', 'Unknown',
@ -20,14 +21,6 @@ device_types = [
] ]
def normalize_datetime(dt: str) -> Optional[datetime]:
if not dt:
return
if dt.endswith('Z'):
dt = dt[:-1] + '+00:00'
return datetime.fromisoformat(dt)
class SpotifySchema(Schema): class SpotifySchema(Schema):
@staticmethod @staticmethod
def _normalize_timestamp(t: Union[str, datetime]) -> datetime: def _normalize_timestamp(t: Union[str, datetime]) -> datetime:

14
platypush/schemas/sun.py Normal file
View file

@ -0,0 +1,14 @@
from marshmallow import fields
from marshmallow.schema import Schema
class SunEventsSchema(Schema):
sunrise = fields.DateTime(metadata=dict(description='Next sunrise time'))
sunset = fields.DateTime(metadata=dict(description='Next sunset time'))
solar_noon = fields.DateTime(metadata=dict(description='Next solar noon time'))
civil_twilight_begin = fields.DateTime(metadata=dict(description='Next civil twilight start time'))
civil_twilight_end = fields.DateTime(metadata=dict(description='Next civil twilight end time'))
nautical_twilight_begin = fields.DateTime(metadata=dict(description='Next nautical twilight start time'))
nautical_twilight_end = fields.DateTime(metadata=dict(description='Next nautical twilight end time'))
astronomical_twilight_begin = fields.DateTime(metadata=dict(description='Next astronomical twilight start time'))
astronomical_twilight_end = fields.DateTime(metadata=dict(description='Next astronomical twilight end time'))