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.
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
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.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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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 """
|
||||||
|
|
||||||
|
|
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 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
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 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
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