forked from platypush/platypush
Added sun
plugin [closes #194]
This commit is contained in:
parent
47228f2432
commit
371dd6da0a
13 changed files with 248 additions and 13 deletions
|
@ -3,6 +3,12 @@
|
|||
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.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added `sun` plugin for sunrise/sunset events.
|
||||
|
||||
## [0.21.2] - 2021-07-20
|
||||
|
||||
### Added
|
||||
|
|
|
@ -54,6 +54,7 @@ Events
|
|||
platypush/events/serial.rst
|
||||
platypush/events/sound.rst
|
||||
platypush/events/stt.rst
|
||||
platypush/events/sun.rst
|
||||
platypush/events/tensorflow.rst
|
||||
platypush/events/todoist.rst
|
||||
platypush/events/torrent.rst
|
||||
|
|
5
docs/source/platypush/events/sun.rst
Normal file
5
docs/source/platypush/events/sun.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``platypush.message.event.sun``
|
||||
===============================
|
||||
|
||||
.. automodule:: platypush.message.event.sun
|
||||
:members:
|
5
docs/source/platypush/plugins/sun.rst
Normal file
5
docs/source/platypush/plugins/sun.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
``sun``
|
||||
=======
|
||||
|
||||
.. automodule:: platypush.plugins.sun
|
||||
:members:
|
|
@ -119,6 +119,7 @@ Plugins
|
|||
platypush/plugins/stt.deepspeech.rst
|
||||
platypush/plugins/stt.picovoice.hotword.rst
|
||||
platypush/plugins/stt.picovoice.speech.rst
|
||||
platypush/plugins/sun.rst
|
||||
platypush/plugins/switch.rst
|
||||
platypush/plugins/switch.tplink.rst
|
||||
platypush/plugins/switch.wemo.rst
|
||||
|
|
|
@ -12,7 +12,7 @@ import sys
|
|||
|
||||
from .bus.redis import RedisBus
|
||||
from .config import Config
|
||||
from .context import register_backends
|
||||
from .context import register_backends, register_plugins
|
||||
from .cron.scheduler import CronScheduler
|
||||
from .event.processor import EventProcessor
|
||||
from .logger import Logger
|
||||
|
@ -20,8 +20,7 @@ from .message.event import Event
|
|||
from .message.event.application import ApplicationStartedEvent
|
||||
from .message.request import Request
|
||||
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>'
|
||||
__version__ = '0.21.2'
|
||||
|
@ -160,9 +159,15 @@ class Daemon:
|
|||
|
||||
def stop_app(self):
|
||||
""" Stops the backends and the bus """
|
||||
from .plugins import RunnablePlugin
|
||||
|
||||
for backend in self.backends.values():
|
||||
backend.stop()
|
||||
|
||||
for plugin in get_enabled_plugins().values():
|
||||
if isinstance(plugin, RunnablePlugin):
|
||||
plugin.stop()
|
||||
|
||||
self.bus.stop()
|
||||
if self.cron_scheduler:
|
||||
self.cron_scheduler.stop()
|
||||
|
@ -184,6 +189,9 @@ class Daemon:
|
|||
for backend in self.backends.values():
|
||||
backend.start()
|
||||
|
||||
# Initialize the plugins
|
||||
register_plugins(bus=self.bus)
|
||||
|
||||
# Start the cron scheduler
|
||||
if Config.get_cronjobs():
|
||||
self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs())
|
||||
|
|
|
@ -5,6 +5,7 @@ import logging
|
|||
from threading import RLock
|
||||
|
||||
from ..config import Config
|
||||
from ..utils import get_enabled_plugins
|
||||
|
||||
logger = logging.getLogger('platypush:context')
|
||||
|
||||
|
@ -21,6 +22,7 @@ plugins_init_locks = {}
|
|||
# Reference to the main application bus
|
||||
main_bus = None
|
||||
|
||||
|
||||
def register_backends(bus=None, global_scope=False, **kwargs):
|
||||
""" Initialize the backend objects based on the configuration and returns
|
||||
a name -> backend_instance map.
|
||||
|
@ -59,6 +61,16 @@ def register_backends(bus=None, global_scope=False, **kwargs):
|
|||
|
||||
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):
|
||||
""" Returns the backend instance identified by name if it exists """
|
||||
|
||||
|
|
36
platypush/message/event/sun.py
Normal file
36
platypush/message/event/sun.py
Normal 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:
|
|
@ -1,10 +1,15 @@
|
|||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
from abc import ABC
|
||||
from functools import wraps
|
||||
from typing import Optional
|
||||
|
||||
from platypush.bus import Bus
|
||||
from platypush.event import EventGenerator
|
||||
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):
|
||||
|
@ -53,4 +58,56 @@ class Plugin(EventGenerator):
|
|||
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:
|
||||
|
|
87
platypush/plugins/sun.py
Normal file
87
platypush/plugins/sun.py
Normal 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()
|
||||
})
|
|
@ -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)
|
|
@ -1,11 +1,12 @@
|
|||
from datetime import datetime
|
||||
from typing import Union, Optional
|
||||
from typing import Union
|
||||
|
||||
from marshmallow import fields, pre_dump
|
||||
from marshmallow.schema import Schema
|
||||
from marshmallow.validate import OneOf, Range
|
||||
|
||||
from platypush.plugins.media import PlayerState
|
||||
from platypush.schemas import normalize_datetime
|
||||
|
||||
device_types = [
|
||||
'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):
|
||||
@staticmethod
|
||||
def _normalize_timestamp(t: Union[str, datetime]) -> datetime:
|
||||
|
|
14
platypush/schemas/sun.py
Normal file
14
platypush/schemas/sun.py
Normal 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'))
|
Loading…
Reference in a new issue