Compare commits

...

127 Commits

Author SHA1 Message Date
Fabio Manganiello 486cd66885
More LINTs 2022-10-23 21:23:19 +02:00
Fabio Manganiello 72c7444a45
LINT 2022-10-23 18:23:20 +02:00
Fabio Manganiello 951950c864
Added dimmer entities 2022-10-23 00:30:32 +02:00
Fabio Manganiello d7278857e5
Ensure that no records with duplicate key exist within an SQLAlchemy session before flushing 2022-10-23 00:28:42 +02:00
Fabio Manganiello 3e6ebdd23b
Don't store/show the state of write-only toggle switches 2022-10-23 00:28:01 +02:00
Fabio Manganiello 8cd5cb3338
The Slider should only react to @input events 2022-10-23 00:26:59 +02:00
Fabio Manganiello 1af7ece881
Added deprecation notice for `zwave` plugin and backend (use `zwave.mqtt` instead) 2022-10-22 19:17:58 +02:00
Fabio Manganiello 5c68365188
Better management for entity error icons 2022-10-14 23:37:36 +02:00
Fabio Manganiello 7f575bacaa
Implemented the new zwavejs2mqtt features for adding and removing nodes 2022-10-14 23:28:02 +02:00
Fabio Manganiello 5995d045e1
Merge branch 'master' into 29-generic-entities-support 2022-10-14 20:57:13 +02:00
Fabio Manganiello c89ed24f4b
Updated webapp dist files 2022-10-12 03:07:17 +02:00
Fabio Manganiello 1b791156bd
Proper support for color zigbee lights 2022-10-12 03:00:42 +02:00
Fabio Manganiello e617fc75d4
Fixed slider ranges and label 2022-10-12 02:59:50 +02:00
Fabio Manganiello 041f64c80f
Dirty workaround to prevent redefinition of SQLAlchemy ORM model classes 2022-10-10 01:38:15 +02:00
Fabio Manganiello aa5b52db2f
FIX: Still redirect to /register by default if no users have been created 2022-10-10 01:36:28 +02:00
Fabio Manganiello 5f09d449f4
`extend_existing=True` for entity tables 2022-10-09 23:15:50 +02:00
Fabio Manganiello 6ec8a991df
Fixed tests 2022-10-08 15:18:26 +02:00
Fabio Manganiello 958ef6b987
Better entity modal padding 2022-10-07 11:12:30 +02:00
Fabio Manganiello 16c55b45f6
updated dist files 2022-10-07 11:12:13 +02:00
Fabio Manganiello b9b7404230
Web panel improvements.
- Don't return a redirect to the login page if an authentication failed
  over a JSON endpoint - instead, return a JSON payload with the error.

- Added support for additional fonts.

- Re-designed the login/registration page.

- Updated caniuse database.
2022-10-07 02:24:29 +02:00
Fabio Manganiello c0ffea681f
updated dist files 2022-10-07 02:23:12 +02:00
Fabio Manganiello 2aab1d090d
Increased maxkb limit 2022-10-07 02:23:04 +02:00
Fabio Manganiello 2cc80e7f16
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-10-07 00:05:54 +02:00
Fabio Manganiello deb25196d2
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-09-28 02:17:10 +02:00
Fabio Manganiello 1880a99052
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-08-29 01:41:47 +02:00
Fabio Manganiello 3513ee3e1c
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-07-08 23:13:36 +02:00
Fabio Manganiello 0d0995d71d
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-06-02 20:58:34 +02:00
Fabio Manganiello 2898a33752
s/click_url/url/g in ntfy message definitions 2022-06-02 00:36:14 +02:00
Fabio Manganiello 0919a0055d
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-06-02 00:13:43 +02:00
Fabio Manganiello 5b3e1317f4
Only refresh entities that are visible on the interface 2022-05-30 09:23:25 +02:00
Fabio Manganiello 1df71cb54a
Proper support for light entities on smartthings 2022-05-30 09:23:05 +02:00
Fabio Manganiello 0689e05e96
Apply the light color to the icon fill instead of the bulb icon itself 2022-05-30 09:18:19 +02:00
Fabio Manganiello 89560e7c38
Only include entities associated to enabled plugins or with no plugins in `entities.get` 2022-05-29 23:59:46 +02:00
Fabio Manganiello 30dfdeecb0
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-05-25 10:11:57 +02:00
Fabio Manganiello f57f940d57
Made _is_switch more resilient against rogue Z-Wave values 2022-05-01 22:18:46 +02:00
Fabio Manganiello 117f92e5b4
Deprecated the `light.hue` backend
The polling logic has been moved to the `light.hue` plugin itself
instead, so it's no longer required to have both a plugin and a backend
enabled in order to fully manage a Hue bridge.
2022-05-01 21:55:35 +02:00
Fabio Manganiello a5541c33b0
Added support for light entities in zigbee.mqtt
TODO: Support for colors (I don't have a color Zigbee bulb to test it on yet)
2022-05-01 21:10:54 +02:00
Fabio Manganiello b23f45f45e
Process a zigbee entity update event with all the properties, not only the ones that have been changed 2022-05-01 21:09:13 +02:00
Fabio Manganiello 088cf23958
Do not emit input event from the light component upon update
It may be an incomplete update that breaks the UI, and it will be
overwritten by the backend event anyway
2022-05-01 21:08:02 +02:00
Fabio Manganiello e8f4b7c10e
CSS adjustments 2022-05-01 15:44:57 +02:00
Fabio Manganiello dd12d57552
Added light UI entity component 2022-05-01 15:35:20 +02:00
Fabio Manganiello 5aa3750807
Re-sync the list of entities when the entities component is mounted 2022-05-01 15:34:45 +02:00
Fabio Manganiello f760d44224
Refactored/simplified UI code for entities management 2022-05-01 15:34:15 +02:00
Fabio Manganiello 8d91fec771
Better implementation for light.hue.set_lights 2022-05-01 15:33:12 +02:00
Fabio Manganiello c22c17a55d
More flexible implementation for LightPlugin abstract methods 2022-05-01 15:31:45 +02:00
Fabio Manganiello 46df3a6a98
FIX: `reachable` is an attribute of `state` 2022-05-01 01:58:05 +02:00
Fabio Manganiello 8e06b8c727
Fixed range scaling on Slider component 2022-04-30 23:40:14 +02:00
Fabio Manganiello 30a024befb
Manage hue/sat/bri/ct light ranges on the light entity object itself 2022-04-30 19:38:50 +02:00
Fabio Manganiello b16af0a97f
Include entity `data` attributes in the entity info modal 2022-04-30 16:39:37 +02:00
Fabio Manganiello c7970842d7
Disable logging by default for entity events (they can be quite spammy) 2022-04-30 02:13:20 +02:00
Fabio Manganiello 7df67aca82
updated_at should have utcnow() onupdate, not now() 2022-04-30 01:48:55 +02:00
Fabio Manganiello d29b377cf1
Exclude deleted lights/groups/scenes from the returned lists 2022-04-30 01:39:39 +02:00
Fabio Manganiello 8d57cf06c2
Major refactor for the `light.hue` plugin.
- Added support for lights as native platform entities.
- Improved performance by using the JSON API objects whenever possible
  to interact with the bridge instead of the native Python objects,
  which perform a bunch of lazy API calls under the hood resulting in
  degraded performance.
- Fixed lights animation attributes by setting only the ones actually
  supported by a light.
- Several LINT fixes.
2022-04-30 01:07:00 +02:00
Fabio Manganiello 975d37c562
Added relevant attributes to `light` entities 2022-04-29 23:29:04 +02:00
Fabio Manganiello 90f067de61
Added `reachable` flag to device entities 2022-04-29 23:27:35 +02:00
Fabio Manganiello f45df5d4d3
Explictly cast entity IDs to strings when coalescing entity updates
Some plugins may represent entity IDs as integers, while the database
maps external IDs to strings. This may result in entities being
incorrectly mapped during merging. Casting to string prevents these
type-related ambiguities.
2022-04-29 23:24:28 +02:00
Fabio Manganiello 975991ba69
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-04-29 16:53:41 +02:00
Fabio Manganiello d22fbcd9db
Merge branch 'master' into 191-support-for-general-entities-backend-and-plugin 2022-04-28 01:58:24 +02:00
Fabio Manganiello 47f8520f3b
Added support for description/read_only/write_only on entity level 2022-04-24 22:18:29 +02:00
Fabio Manganiello d261b9bb9b
Frontend support for entities deletion 2022-04-24 21:40:10 +02:00
Fabio Manganiello 9981cc4746
Backend support for entities deletion 2022-04-24 21:38:45 +02:00
Fabio Manganiello 3e4b13d20f
Added standard Vue component for confirm dialogs 2022-04-24 21:34:39 +02:00
Fabio Manganiello 321a61d06d
Align .section.right content to the right 2022-04-24 11:30:52 +02:00
Fabio Manganiello b22df768eb
Fixed entity icon alignment on mobile 2022-04-24 01:42:14 +02:00
Fabio Manganiello 8e2154f2b5
Do not overwrite an entity's state from an event if the state was not sampled 2022-04-24 01:41:45 +02:00
Fabio Manganiello a9751f21f1
`entities` should be the default view when the web panel is opened 2022-04-24 01:40:34 +02:00
Fabio Manganiello 135965176d
Support for entity icon color change 2022-04-23 17:52:21 +02:00
Fabio Manganiello ef6b57df31
Added entity info modal and (partial) support for renaming entities 2022-04-23 01:01:14 +02:00
Fabio Manganiello 7d4bd20df0
Support for individual entity group refresh 2022-04-19 23:56:49 +02:00
Fabio Manganiello e6bfa1c50f
Better dynamic entities discovery 2022-04-13 11:25:14 +02:00
Fabio Manganiello 332c91252c
zwave.mqtt.status renamed to controller_status, while status should return the current state of the values 2022-04-12 23:44:14 +02:00
Fabio Manganiello b35c761a43
Fixed entities panel mobile layout 2022-04-12 22:24:19 +02:00
Fabio Manganiello 08c0779347
<style> on entity components should be scoped 2022-04-12 16:00:31 +02:00
Fabio Manganiello 595ebe49ca
Support for entity scan timeout errors and visual error handling 2022-04-12 15:58:19 +02:00
Fabio Manganiello 548d487e73
Publish a switch entity from zigbee.mqtt only if the update includes its state 2022-04-12 14:41:21 +02:00
Fabio Manganiello 20530c2b6d
Loading events are now synchronized both ways upon entity action/refresh 2022-04-12 01:10:09 +02:00
Fabio Manganiello 9ddcf5eaeb
Implemented entities refresh on the UI 2022-04-12 00:43:22 +02:00
Fabio Manganiello 2aa8778078
Do not process EntityUpdateEvents only in case of payload changes
The UI relies on these events upon refresh to detect if a device is
still reacheable. Therefore, we shouldn't mask them if we don't detect
any changes with the current entity configuration/state.
2022-04-12 00:41:20 +02:00
Fabio Manganiello 72617b4b75
Handle EntityUpdateEvents on the UI 2022-04-11 23:16:29 +02:00
Fabio Manganiello be4d1e8e01
Proper support for native entities in zigbee.mqtt integration 2022-04-11 21:16:45 +02:00
Fabio Manganiello db4ad5825e
Fire an EntityUpdateEvent when the zwave.mqtt backend gets a value changed message 2022-04-11 01:40:49 +02:00
Fabio Manganiello 4471001110
smartthings.toggle should properly publish the updated entity 2022-04-11 00:43:31 +02:00
Fabio Manganiello f17245e8c7
Send an EntityUpdateEvent only if an entity has already been persisted
If an event comes from an entity that hasn't been persisted yet on the
internal storage then we wait for the entity record to be committed
before firing an event. It's better to wait a couple of seconds for the
database to synchronize rather than dealing with entity events with
incomplete objects.
2022-04-11 00:38:11 +02:00
Fabio Manganiello 67ff585f6c
Entities engine improvements
- Added cache support to prevent duplicate EntityUpdateEvents
- The cache is smartly pre-populated and kept up-to-date, so it's
  possible to trigger events as soon as the entities are published by
  the plugin (not only when the records are flushed to the internal db)
2022-04-11 00:01:21 +02:00
Fabio Manganiello 17615ff028
Support for multiple entity types/plugins filter on entities.get 2022-04-10 21:23:03 +02:00
Fabio Manganiello 532217be12
Support for filtering entities by search string 2022-04-10 17:57:51 +02:00
Fabio Manganiello f301fd7e69
Added standard NoItems component to handle visualization of no-results divs 2022-04-10 14:27:32 +02:00
Fabio Manganiello 58861afb1c
Added entities panel 2022-04-10 13:07:36 +02:00
Fabio Manganiello 8ec9c8f203
Added standard component for icons 2022-04-10 13:07:01 +02:00
Fabio Manganiello 3435f591eb
Support for keep-open-on-item-click and icon URLs on dropdown elements 2022-04-10 01:57:39 +02:00
Fabio Manganiello 19223bbbe1
Added SmartThings icon 2022-04-10 01:56:47 +02:00
Fabio Manganiello 453652ef76
Updated plugin icons 2022-04-10 01:50:45 +02:00
Fabio Manganiello b2ff66aa62
Added mixins to capitalize/prettify text 2022-04-10 01:50:13 +02:00
Fabio Manganiello 655d56f4da
Upgraded font-awesome to 6.x 2022-04-10 01:49:14 +02:00
Fabio Manganiello f52b556219
- icon_class should not be part of the backend model
- Interaction with entities should occur through the `entities.action`
  method, not by implementing native methods on each of the model
  objects
2022-04-08 16:49:47 +02:00
Fabio Manganiello 947b50b937
Added meta as a JSON field on the Entity table
Metadata attributes can now be defined and overridden on the object
itself, as well as on the database. Note that db settings will always
take priority in case of value conflicts.
2022-04-07 22:11:31 +02:00
Fabio Manganiello db7c2095ea
Implemented meta property for entities (for now it only include `icon_class`) 2022-04-07 18:09:25 +02:00
Fabio Manganiello e40b668380
Added missing docs 2022-04-07 01:49:13 +02:00
Fabio Manganiello d3dc86a5e2
Added documentation for plugin/entity type registry 2022-04-07 01:47:42 +02:00
Fabio Manganiello 28026b0428
Trigger an EntityUpdateEvent when an entity state changes 2022-04-07 01:46:37 +02:00
Fabio Manganiello 44707731a8
Normalize UTC timezone on all the entity timestamps 2022-04-07 01:13:29 +02:00
Fabio Manganiello 948f37afd4
Filter by configured/enabled plugins when returning the entity/plugin registry 2022-04-07 01:04:06 +02:00
Fabio Manganiello 3b4f7d3dad
Added entities plugin to query/action entities 2022-04-07 00:22:54 +02:00
Fabio Manganiello 2eeb1d4fea
Entity objects are now JSON-able 2022-04-07 00:21:54 +02:00
Fabio Manganiello 26ffc0b0e1
Use Redis instead of an in-process map to store the entity/plugin registry
This is particularly useful when we want to access the registry from
another process, like the web server or an external script.
2022-04-07 00:18:11 +02:00
Fabio Manganiello 7b1a63e287
Make sure that flake8 and black don't step on each other's toes 2022-04-07 00:17:39 +02:00
Fabio Manganiello 1c6ff2fa49
(actually, the other way around is better) 2022-04-06 23:56:10 +02:00
Fabio Manganiello d311629403
black validation should run before flake8 2022-04-06 23:48:27 +02:00
Fabio Manganiello d52ae2fb80
Implemented RunnablePlugin.wait_stop() utility method 2022-04-05 23:33:02 +02:00
Fabio Manganiello 061268cdaf
Support for direct actions on native entities [WIP] 2022-04-05 23:22:54 +02:00
Fabio Manganiello 91ff47167b
Don't terminate the entities engine thread if a batch of entity records fails 2022-04-05 23:04:57 +02:00
Fabio Manganiello fe0f3202fe
columns should be a property of the Entity object 2022-04-05 23:04:19 +02:00
Fabio Manganiello 8a70f1d38e
Replaced deprecated sqlalchemy.ext.declarative with sqlalchemy.orm 2022-04-05 22:47:44 +02:00
Fabio Manganiello 4b7eeaa4ed
Smarter merging of entities with the same key before they are committed 2022-04-05 21:17:58 +02:00
Fabio Manganiello b43ed169c7
Added support for switches as native entities to zwave.mqtt plugin 2022-04-05 20:22:47 +02:00
Fabio Manganiello 0dac2c0e92
Fixed handling of possible null device definition in zigbee.mqtt 2022-04-05 00:31:04 +02:00
Fabio Manganiello 28b3672432
Added native support for switch entities to the zigbee.mqtt plugin. 2022-04-05 00:07:55 +02:00
Fabio Manganiello 9f2793118b
black fix 2022-04-04 22:43:04 +02:00
Fabio Manganiello 9d9ec1dc59
Added native support for switch entities to the smartthings plugin 2022-04-04 22:41:04 +02:00
Fabio Manganiello b9c78ad913
Added native support for switch entities to switchbot.bluetooth plugin 2022-04-04 21:12:59 +02:00
Fabio Manganiello 91ff8d811f
Added native entities support in switchbot plugin 2022-04-04 20:56:28 +02:00
Fabio Manganiello 783238642d
Skip string and underscore normalization in black 2022-04-04 20:56:28 +02:00
Fabio Manganiello 53da19b638
Added entities engine support to WeMo switch plugin 2022-04-04 17:22:55 +02:00
Fabio Manganiello 7459f0115b
Added more pre-commit hooks 2022-04-04 17:22:54 +02:00
Fabio Manganiello 2c4c27855d
Added `.exception` action to logger plugin 2022-04-04 17:22:54 +02:00
Fabio Manganiello 9c25a131fa
get_bus() should return a default RedisBus() instance if the main bus is not registered 2022-04-04 17:22:54 +02:00
Fabio Manganiello 4ee7e4db29
Basic support for entities on the local db and implemented support for switch entities on the tplink plugin 2022-04-04 16:50:17 +02:00
541 changed files with 8138 additions and 2275 deletions

View File

@ -6,11 +6,12 @@ repos:
hooks:
# - id: trailing-whitespace
# - id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-xml
- id: check-symlinks
- id: check-added-large-files
- id: check-yaml
- id: check-json
- id: check-xml
- id: check-symlinks
- id: check-added-large-files
args: ['--maxkb=1500']
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
rev: v1.1.2

View File

@ -20,6 +20,7 @@ Events
platypush/events/custom.rst
platypush/events/dbus.rst
platypush/events/distance.rst
platypush/events/entities.rst
platypush/events/file.rst
platypush/events/foursquare.rst
platypush/events/geo.rst

View File

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

View File

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

View File

@ -32,6 +32,7 @@ Plugins
platypush/plugins/db.rst
platypush/plugins/dbus.rst
platypush/plugins/dropbox.rst
platypush/plugins/entities.rst
platypush/plugins/esp.rst
platypush/plugins/ffmpeg.rst
platypush/plugins/file.rst

View File

@ -9,11 +9,13 @@ import argparse
import logging
import os
import sys
from typing import Optional
from .bus.redis import RedisBus
from .config import Config
from .context import register_backends, register_plugins
from .cron.scheduler import CronScheduler
from .entities import init_entities_engine, EntitiesEngine
from .event.processor import EventProcessor
from .logger import Logger
from .message.event import Event
@ -96,6 +98,7 @@ class Daemon:
self.no_capture_stdout = no_capture_stdout
self.no_capture_stderr = no_capture_stderr
self.event_processor = EventProcessor()
self.entities_engine: Optional[EntitiesEngine] = None
self.requests_to_process = requests_to_process
self.processed_requests = 0
self.cron_scheduler = None
@ -199,16 +202,25 @@ class Daemon:
"""Stops the backends and the bus"""
from .plugins import RunnablePlugin
for backend in self.backends.values():
backend.stop()
if self.backends:
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.bus:
self.bus.stop()
self.bus = None
if self.cron_scheduler:
self.cron_scheduler.stop()
self.cron_scheduler = None
if self.entities_engine:
self.entities_engine.stop()
self.entities_engine = None
def run(self):
"""Start the daemon"""
@ -230,6 +242,9 @@ class Daemon:
# Initialize the plugins
register_plugins(bus=self.bus)
# Initialize the entities engine
self.entities_engine = init_entities_engine()
# Start the cron scheduler
if Config.get_cronjobs():
self.cron_scheduler = CronScheduler(jobs=Config.get_cronjobs())

View File

@ -3,8 +3,7 @@ import os
from typing import Optional, Union, List, Dict, Any
from sqlalchemy import create_engine, Column, Integer, String, DateTime
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base
from platypush.backend import Backend
from platypush.config import Config
@ -17,10 +16,10 @@ Session = scoped_session(sessionmaker())
class Covid19Update(Base):
""" Models the Covid19Data table """
"""Models the Covid19Data table"""
__tablename__ = 'covid19data'
__table_args__ = ({'sqlite_autoincrement': True})
__table_args__ = {'sqlite_autoincrement': True}
country = Column(String, primary_key=True)
confirmed = Column(Integer, nullable=False, default=0)
@ -40,7 +39,12 @@ class Covid19Backend(Backend):
"""
# noinspection PyProtectedMember
def __init__(self, country: Optional[Union[str, List[str]]], poll_seconds: Optional[float] = 3600.0, **kwargs):
def __init__(
self,
country: Optional[Union[str, List[str]]],
poll_seconds: Optional[float] = 3600.0,
**kwargs
):
"""
:param country: Default country (or list of countries) to retrieve the stats for. It can either be the full
country name or the country code. Special values:
@ -56,7 +60,9 @@ class Covid19Backend(Backend):
super().__init__(poll_seconds=poll_seconds, **kwargs)
self._plugin: Covid19Plugin = get_plugin('covid19')
self.country: List[str] = self._plugin._get_countries(country)
self.workdir = os.path.join(os.path.expanduser(Config.get('workdir')), 'covid19')
self.workdir = os.path.join(
os.path.expanduser(Config.get('workdir')), 'covid19'
)
self.dbfile = os.path.join(self.workdir, 'data.db')
os.makedirs(self.workdir, exist_ok=True)
@ -67,22 +73,30 @@ class Covid19Backend(Backend):
self.logger.info('Stopped Covid19 backend')
def _process_update(self, summary: Dict[str, Any], session: Session):
update_time = datetime.datetime.fromisoformat(summary['Date'].replace('Z', '+00:00'))
update_time = datetime.datetime.fromisoformat(
summary['Date'].replace('Z', '+00:00')
)
self.bus.post(Covid19UpdateEvent(
country=summary['Country'],
country_code=summary['CountryCode'],
confirmed=summary['TotalConfirmed'],
deaths=summary['TotalDeaths'],
recovered=summary['TotalRecovered'],
update_time=update_time,
))
self.bus.post(
Covid19UpdateEvent(
country=summary['Country'],
country_code=summary['CountryCode'],
confirmed=summary['TotalConfirmed'],
deaths=summary['TotalDeaths'],
recovered=summary['TotalRecovered'],
update_time=update_time,
)
)
session.merge(Covid19Update(country=summary['CountryCode'],
confirmed=summary['TotalConfirmed'],
deaths=summary['TotalDeaths'],
recovered=summary['TotalRecovered'],
last_updated_at=update_time))
session.merge(
Covid19Update(
country=summary['CountryCode'],
confirmed=summary['TotalConfirmed'],
deaths=summary['TotalDeaths'],
recovered=summary['TotalRecovered'],
last_updated_at=update_time,
)
)
def loop(self):
# noinspection PyUnresolvedReferences
@ -90,23 +104,30 @@ class Covid19Backend(Backend):
if not summaries:
return
engine = create_engine('sqlite:///{}'.format(self.dbfile), connect_args={'check_same_thread': False})
engine = create_engine(
'sqlite:///{}'.format(self.dbfile),
connect_args={'check_same_thread': False},
)
Base.metadata.create_all(engine)
Session.configure(bind=engine)
session = Session()
last_records = {
record.country: record
for record in session.query(Covid19Update).filter(Covid19Update.country.in_(self.country)).all()
for record in session.query(Covid19Update)
.filter(Covid19Update.country.in_(self.country))
.all()
}
for summary in summaries:
country = summary['CountryCode']
last_record = last_records.get(country)
if not last_record or \
summary['TotalConfirmed'] != last_record.confirmed or \
summary['TotalDeaths'] != last_record.deaths or \
summary['TotalRecovered'] != last_record.recovered:
if (
not last_record
or summary['TotalConfirmed'] != last_record.confirmed
or summary['TotalDeaths'] != last_record.deaths
or summary['TotalRecovered'] != last_record.recovered
):
self._process_update(summary=summary, session=session)
session.commit()

View File

@ -6,15 +6,28 @@ from typing import Optional, List
import requests
from sqlalchemy import create_engine, Column, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base
from platypush.backend import Backend
from platypush.config import Config
from platypush.message.event.github import GithubPushEvent, GithubCommitCommentEvent, GithubCreateEvent, \
GithubDeleteEvent, GithubEvent, GithubForkEvent, GithubWikiEvent, GithubIssueCommentEvent, GithubIssueEvent, \
GithubMemberEvent, GithubPublicEvent, GithubPullRequestEvent, GithubPullRequestReviewCommentEvent, \
GithubReleaseEvent, GithubSponsorshipEvent, GithubWatchEvent
from platypush.message.event.github import (
GithubPushEvent,
GithubCommitCommentEvent,
GithubCreateEvent,
GithubDeleteEvent,
GithubEvent,
GithubForkEvent,
GithubWikiEvent,
GithubIssueCommentEvent,
GithubIssueEvent,
GithubMemberEvent,
GithubPublicEvent,
GithubPullRequestEvent,
GithubPullRequestReviewCommentEvent,
GithubReleaseEvent,
GithubSponsorshipEvent,
GithubWatchEvent,
)
Base = declarative_base()
Session = scoped_session(sessionmaker())
@ -71,8 +84,17 @@ class GithubBackend(Backend):
_base_url = 'https://api.github.com'
def __init__(self, user: str, user_token: str, repos: Optional[List[str]] = None, org: Optional[str] = None,
poll_seconds: int = 60, max_events_per_scan: Optional[int] = 10, *args, **kwargs):
def __init__(
self,
user: str,
user_token: str,
repos: Optional[List[str]] = None,
org: Optional[str] = None,
poll_seconds: int = 60,
max_events_per_scan: Optional[int] = 10,
*args,
**kwargs
):
"""
If neither ``repos`` nor ``org`` is specified then the backend will monitor all new events on user level.
@ -102,17 +124,23 @@ class GithubBackend(Backend):
def _request(self, uri: str, method: str = 'get') -> dict:
method = getattr(requests, method.lower())
return method(self._base_url + uri, auth=(self.user, self.user_token),
headers={'Accept': 'application/vnd.github.v3+json'}).json()
return method(
self._base_url + uri,
auth=(self.user, self.user_token),
headers={'Accept': 'application/vnd.github.v3+json'},
).json()
def _init_db(self):
engine = create_engine('sqlite:///{}'.format(self.dbfile), connect_args={'check_same_thread': False})
engine = create_engine(
'sqlite:///{}'.format(self.dbfile),
connect_args={'check_same_thread': False},
)
Base.metadata.create_all(engine)
Session.configure(bind=engine)
@staticmethod
def _to_datetime(time_string: str) -> datetime.datetime:
""" Convert ISO 8061 string format with leading 'Z' into something understandable by Python """
"""Convert ISO 8061 string format with leading 'Z' into something understandable by Python"""
return datetime.datetime.fromisoformat(time_string[:-1] + '+00:00')
@staticmethod
@ -128,7 +156,11 @@ class GithubBackend(Backend):
def _get_last_event_time(self, uri: str):
with self.db_lock:
record = self._get_or_create_resource(uri=uri, session=Session())
return record.last_updated_at.replace(tzinfo=datetime.timezone.utc) if record.last_updated_at else None
return (
record.last_updated_at.replace(tzinfo=datetime.timezone.utc)
if record.last_updated_at
else None
)
def _update_last_event_time(self, uri: str, last_updated_at: datetime.datetime):
with self.db_lock:
@ -158,9 +190,18 @@ class GithubBackend(Backend):
'WatchEvent': GithubWatchEvent,
}
event_type = event_mapping[event['type']] if event['type'] in event_mapping else GithubEvent
return event_type(event_type=event['type'], actor=event['actor'], repo=event.get('repo', {}),
payload=event['payload'], created_at=cls._to_datetime(event['created_at']))
event_type = (
event_mapping[event['type']]
if event['type'] in event_mapping
else GithubEvent
)
return event_type(
event_type=event['type'],
actor=event['actor'],
repo=event.get('repo', {}),
payload=event['payload'],
created_at=cls._to_datetime(event['created_at']),
)
def _events_monitor(self, uri: str, method: str = 'get'):
def thread():
@ -175,7 +216,10 @@ class GithubBackend(Backend):
fired_events = []
for event in events:
if self.max_events_per_scan and len(fired_events) >= self.max_events_per_scan:
if (
self.max_events_per_scan
and len(fired_events) >= self.max_events_per_scan
):
break
event_time = self._to_datetime(event['created_at'])
@ -189,14 +233,19 @@ class GithubBackend(Backend):
for event in fired_events:
self.bus.post(event)
self._update_last_event_time(uri=uri, last_updated_at=new_last_event_time)
self._update_last_event_time(
uri=uri, last_updated_at=new_last_event_time
)
except Exception as e:
self.logger.warning('Encountered exception while fetching events from {}: {}'.format(
uri, str(e)))
self.logger.warning(
'Encountered exception while fetching events from {}: {}'.format(
uri, str(e)
)
)
self.logger.exception(e)
finally:
if self.wait_stop(timeout=self.poll_seconds):
break
if self.wait_stop(timeout=self.poll_seconds):
break
return thread
@ -206,12 +255,30 @@ class GithubBackend(Backend):
if self.repos:
for repo in self.repos:
monitors.append(threading.Thread(target=self._events_monitor('/networks/{repo}/events'.format(repo=repo))))
monitors.append(
threading.Thread(
target=self._events_monitor(
'/networks/{repo}/events'.format(repo=repo)
)
)
)
if self.org:
monitors.append(threading.Thread(target=self._events_monitor('/orgs/{org}/events'.format(org=self.org))))
monitors.append(
threading.Thread(
target=self._events_monitor(
'/orgs/{org}/events'.format(org=self.org)
)
)
)
if not (self.repos or self.org):
monitors.append(threading.Thread(target=self._events_monitor('/users/{user}/events'.format(user=self.user))))
monitors.append(
threading.Thread(
target=self._events_monitor(
'/users/{user}/events'.format(user=self.user)
)
)
)
for monitor in monitors:
monitor.start()
@ -222,4 +289,5 @@ class GithubBackend(Backend):
self.logger.info('Github backend terminated')
# vim:sw=4:ts=4:et:

View File

@ -1,6 +1,7 @@
import json
from flask import Blueprint, abort, request, Response
from flask import Blueprint, abort, request
from flask.wrappers import Response
from platypush.backend.http.app import template_folder
from platypush.backend.http.app.utils import authenticate, logger, send_message
@ -14,8 +15,8 @@ __routes__ = [
@execute.route('/execute', methods=['POST'])
@authenticate()
def execute():
@authenticate(json=True)
def execute_route():
"""Endpoint to execute commands"""
try:
msg = json.loads(request.data.decode('utf-8'))

View File

@ -8,22 +8,27 @@ from platypush.backend.http.app import template_folder
img_folder = os.path.join(template_folder, 'img')
fonts_folder = os.path.join(template_folder, 'fonts')
icons_folder = os.path.join(template_folder, 'icons')
resources = Blueprint('resources', __name__, template_folder=template_folder)
favicon = Blueprint('favicon', __name__, template_folder=template_folder)
img = Blueprint('img', __name__, template_folder=template_folder)
icons = Blueprint('icons', __name__, template_folder=template_folder)
fonts = Blueprint('fonts', __name__, template_folder=template_folder)
# Declare routes list
__routes__ = [
resources,
favicon,
img,
icons,
fonts,
]
@resources.route('/resources/<path:path>', methods=['GET'])
def resources_path(path):
""" Custom static resources """
"""Custom static resources"""
path_tokens = path.split('/')
http_conf = Config.get('backend.http')
resource_dirs = http_conf.get('resource_dirs', {})
@ -42,9 +47,11 @@ def resources_path(path):
real_path = real_base_path
file_path = [
s for s in re.sub(
r'^{}(.*)$'.format(base_path), '\\1', path # lgtm [py/regex-injection]
).split('/') if s
s
for s in re.sub(
r'^{}(.*)$'.format(base_path), '\\1', path # lgtm [py/regex-injection]
).split('/')
if s
]
for p in file_path[:-1]:
@ -61,20 +68,26 @@ def resources_path(path):
@favicon.route('/favicon.ico', methods=['GET'])
def serve_favicon():
""" favicon.ico icon """
"""favicon.ico icon"""
return send_from_directory(template_folder, 'favicon.ico')
@img.route('/img/<path:path>', methods=['GET'])
def imgpath(path):
""" Default static images """
"""Default static images"""
return send_from_directory(img_folder, path)
@img.route('/icons/<path:path>', methods=['GET'])
@icons.route('/icons/<path:path>', methods=['GET'])
def iconpath(path):
""" Default static icons """
"""Default static icons"""
return send_from_directory(icons_folder, path)
@fonts.route('/fonts/<path:path>', methods=['GET'])
def fontpath(path):
"""Default fonts"""
return send_from_directory(fonts_folder, path)
# vim:sw=4:ts=4:et:

View File

@ -3,7 +3,8 @@ import logging
import os
from functools import wraps
from flask import abort, request, redirect, Response, current_app
from flask import abort, request, redirect, jsonify, current_app
from flask.wrappers import Response
from redis import Redis
# NOTE: The HTTP service will *only* work on top of a Redis bus. The default
@ -184,7 +185,37 @@ def _authenticate_csrf_token():
)
def authenticate(redirect_page='', skip_auth_methods=None, check_csrf_token=False):
def authenticate(
redirect_page='',
skip_auth_methods=None,
check_csrf_token=False,
json=False,
):
def on_auth_fail(has_users=True):
if json:
if has_users:
return (
jsonify(
{
'message': 'Not logged in',
}
),
401,
)
return (
jsonify(
{
'message': 'Please register a user through '
'the web panel first',
}
),
412,
)
target_page = 'login' if has_users else 'register'
return redirect(f'/{target_page}?redirect={redirect_page or request.url}', 307)
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
@ -213,9 +244,7 @@ def authenticate(redirect_page='', skip_auth_methods=None, check_csrf_token=Fals
if session_auth_ok:
return f(*args, **kwargs)
return redirect(
'/login?redirect=' + (redirect_page or request.url), 307
)
return on_auth_fail()
# CSRF token check
if check_csrf_token:
@ -224,9 +253,7 @@ def authenticate(redirect_page='', skip_auth_methods=None, check_csrf_token=Fals
return abort(403, 'Invalid or missing csrf_token')
if n_users == 0 and 'session' not in skip_methods:
return redirect(
'/register?redirect=' + (redirect_page or request.url), 307
)
return on_auth_fail(has_users=False)
if (
('http' not in skip_methods and http_auth_ok)

View File

@ -2,11 +2,17 @@ import datetime
import enum
import os
from sqlalchemy import create_engine, Column, Integer, String, DateTime, \
Enum, ForeignKey
from sqlalchemy import (
create_engine,
Column,
Integer,
String,
DateTime,
Enum,
ForeignKey,
)
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session, declarative_base
from sqlalchemy.sql.expression import func
from platypush.backend.http.request import HttpRequest
@ -44,18 +50,31 @@ class RssUpdates(HttpRequest):
"""
user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) ' + \
'Chrome/62.0.3202.94 Safari/537.36'
user_agent = (
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) '
+ 'Chrome/62.0.3202.94 Safari/537.36'
)
def __init__(self, url, title=None, headers=None, params=None, max_entries=None,
extract_content=False, digest_format=None, user_agent: str = user_agent,
body_style: str = 'font-size: 22px; ' +
'font-family: "Merriweather", Georgia, "Times New Roman", Times, serif;',
title_style: str = 'margin-top: 30px',
subtitle_style: str = 'margin-top: 10px; page-break-after: always',
article_title_style: str = 'page-break-before: always',
article_link_style: str = 'color: #555; text-decoration: none; border-bottom: 1px dotted',
article_content_style: str = '', *argv, **kwargs):
def __init__(
self,
url,
title=None,
headers=None,
params=None,
max_entries=None,
extract_content=False,
digest_format=None,
user_agent: str = user_agent,
body_style: str = 'font-size: 22px; '
+ 'font-family: "Merriweather", Georgia, "Times New Roman", Times, serif;',
title_style: str = 'margin-top: 30px',
subtitle_style: str = 'margin-top: 10px; page-break-after: always',
article_title_style: str = 'page-break-before: always',
article_link_style: str = 'color: #555; text-decoration: none; border-bottom: 1px dotted',
article_content_style: str = '',
*argv,
**kwargs,
):
"""
:param url: URL to the RSS feed to be monitored.
:param title: Optional title for the feed.
@ -91,7 +110,9 @@ class RssUpdates(HttpRequest):
# If true, then the http.webpage plugin will be used to parse the content
self.extract_content = extract_content
self.digest_format = digest_format.lower() if digest_format else None # Supported formats: html, pdf
self.digest_format = (
digest_format.lower() if digest_format else None
) # Supported formats: html, pdf
os.makedirs(os.path.expanduser(os.path.dirname(self.dbfile)), exist_ok=True)
@ -119,7 +140,11 @@ class RssUpdates(HttpRequest):
@staticmethod
def _get_latest_update(session, source_id):
return session.query(func.max(FeedEntry.published)).filter_by(source_id=source_id).scalar()
return (
session.query(func.max(FeedEntry.published))
.filter_by(source_id=source_id)
.scalar()
)
def _parse_entry_content(self, link):
self.logger.info('Extracting content from {}'.format(link))
@ -130,14 +155,20 @@ class RssUpdates(HttpRequest):
errors = response.errors
if not output:
self.logger.warning('Mercury parser error: {}'.format(errors or '[unknown error]'))
self.logger.warning(
'Mercury parser error: {}'.format(errors or '[unknown error]')
)
return
return output.get('content')
def get_new_items(self, response):
import feedparser
engine = create_engine('sqlite:///{}'.format(self.dbfile), connect_args={'check_same_thread': False})
engine = create_engine(
'sqlite:///{}'.format(self.dbfile),
connect_args={'check_same_thread': False},
)
Base.metadata.create_all(engine)
Session.configure(bind=engine)
@ -157,12 +188,16 @@ class RssUpdates(HttpRequest):
content = u'''
<h1 style="{title_style}">{title}</h1>
<h2 style="{subtitle_style}">Feeds digest generated on {creation_date}</h2>'''.\
format(title_style=self.title_style, title=self.title, subtitle_style=self.subtitle_style,
creation_date=datetime.datetime.now().strftime('%d %B %Y, %H:%M'))
<h2 style="{subtitle_style}">Feeds digest generated on {creation_date}</h2>'''.format(
title_style=self.title_style,
title=self.title,
subtitle_style=self.subtitle_style,
creation_date=datetime.datetime.now().strftime('%d %B %Y, %H:%M'),
)
self.logger.info('Parsed {:d} items from RSS feed <{}>'
.format(len(feed.entries), self.url))
self.logger.info(
'Parsed {:d} items from RSS feed <{}>'.format(len(feed.entries), self.url)
)
for entry in feed.entries:
if not entry.published_parsed:
@ -171,9 +206,10 @@ class RssUpdates(HttpRequest):
try:
entry_timestamp = datetime.datetime(*entry.published_parsed[:6])
if latest_update is None \
or entry_timestamp > latest_update:
self.logger.info('Processed new item from RSS feed <{}>'.format(self.url))
if latest_update is None or entry_timestamp > latest_update:
self.logger.info(
'Processed new item from RSS feed <{}>'.format(self.url)
)
entry.summary = entry.summary if hasattr(entry, 'summary') else None
if self.extract_content:
@ -188,9 +224,13 @@ class RssUpdates(HttpRequest):
<a href="{link}" target="_blank" style="{article_link_style}">{title}</a>
</h1>
<div class="_parsed-content" style="{article_content_style}">{content}</div>'''.format(
article_title_style=self.article_title_style, article_link_style=self.article_link_style,
article_content_style=self.article_content_style, link=entry.link, title=entry.title,
content=entry.content)
article_title_style=self.article_title_style,
article_link_style=self.article_link_style,
article_content_style=self.article_content_style,
link=entry.link,
title=entry.title,
content=entry.content,
)
e = {
'entry_id': entry.id,
@ -207,21 +247,32 @@ class RssUpdates(HttpRequest):
if self.max_entries and len(entries) > self.max_entries:
break
except Exception as e:
self.logger.warning('Exception encountered while parsing RSS ' +
'RSS feed {}: {}'.format(entry.link, str(e)))
self.logger.warning(
'Exception encountered while parsing RSS '
+ f'RSS feed {entry.link}: {e}'
)
self.logger.exception(e)
source_record.last_updated_at = parse_start_time
digest_filename = None
if entries:
self.logger.info('Parsed {} new entries from the RSS feed {}'.format(
len(entries), self.title))
self.logger.info(
'Parsed {} new entries from the RSS feed {}'.format(
len(entries), self.title
)
)
if self.digest_format:
digest_filename = os.path.join(self.workdir, 'cache', '{}_{}.{}'.format(
datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
self.title, self.digest_format))
digest_filename = os.path.join(
self.workdir,
'cache',
'{}_{}.{}'.format(
datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
self.title,
self.digest_format,
),
)
os.makedirs(os.path.dirname(digest_filename), exist_ok=True)
@ -233,12 +284,15 @@ class RssUpdates(HttpRequest):
</head>
<body style="{body_style}">{content}</body>
</html>
'''.format(title=self.title, body_style=self.body_style, content=content)
'''.format(
title=self.title, body_style=self.body_style, content=content
)
with open(digest_filename, 'w', encoding='utf-8') as f:
f.write(content)
elif self.digest_format == 'pdf':
from weasyprint import HTML, CSS
try:
from weasyprint.fonts import FontConfiguration
except ImportError:
@ -246,37 +300,47 @@ class RssUpdates(HttpRequest):
body_style = 'body { ' + self.body_style + ' }'
font_config = FontConfiguration()
css = [CSS('https://fonts.googleapis.com/css?family=Merriweather'),
CSS(string=body_style, font_config=font_config)]
css = [
CSS('https://fonts.googleapis.com/css?family=Merriweather'),
CSS(string=body_style, font_config=font_config),
]
HTML(string=content).write_pdf(digest_filename, stylesheets=css)
else:
raise RuntimeError('Unsupported format: {}. Supported formats: ' +
'html or pdf'.format(self.digest_format))
raise RuntimeError(
f'Unsupported format: {self.digest_format}. Supported formats: html, pdf'
)
digest_entry = FeedDigest(source_id=source_record.id,
format=self.digest_format,
filename=digest_filename)
digest_entry = FeedDigest(
source_id=source_record.id,
format=self.digest_format,
filename=digest_filename,
)
session.add(digest_entry)
self.logger.info('{} digest ready: {}'.format(self.digest_format, digest_filename))
self.logger.info(
'{} digest ready: {}'.format(self.digest_format, digest_filename)
)
session.commit()
self.logger.info('Parsing RSS feed {}: completed'.format(self.title))
return NewFeedEvent(request=dict(self), response=entries,
source_id=source_record.id,
source_title=source_record.title,
title=self.title,
digest_format=self.digest_format,
digest_filename=digest_filename)
return NewFeedEvent(
request=dict(self),
response=entries,
source_id=source_record.id,
source_title=source_record.title,
title=self.title,
digest_format=self.digest_format,
digest_filename=digest_filename,
)
class FeedSource(Base):
""" Models the FeedSource table, containing RSS sources to be parsed """
"""Models the FeedSource table, containing RSS sources to be parsed"""
__tablename__ = 'FeedSource'
__table_args__ = ({'sqlite_autoincrement': True})
__table_args__ = {'sqlite_autoincrement': True}
id = Column(Integer, primary_key=True)
title = Column(String)
@ -285,10 +349,10 @@ class FeedSource(Base):
class FeedEntry(Base):
""" Models the FeedEntry table, which contains RSS entries """
"""Models the FeedEntry table, which contains RSS entries"""
__tablename__ = 'FeedEntry'
__table_args__ = ({'sqlite_autoincrement': True})
__table_args__ = {'sqlite_autoincrement': True}
id = Column(Integer, primary_key=True)
entry_id = Column(String)
@ -301,15 +365,15 @@ class FeedEntry(Base):
class FeedDigest(Base):
""" Models the FeedDigest table, containing feed digests either in HTML
or PDF format """
"""Models the FeedDigest table, containing feed digests either in HTML
or PDF format"""
class DigestFormat(enum.Enum):
html = 1
pdf = 2
__tablename__ = 'FeedDigest'
__table_args__ = ({'sqlite_autoincrement': True})
__table_args__ = {'sqlite_autoincrement': True}
id = Column(Integer, primary_key=True)
source_id = Column(Integer, ForeignKey('FeedSource.id'), nullable=False)
@ -317,4 +381,5 @@ class FeedDigest(Base):
filename = Column(String, nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
# vim:sw=4:ts=4:et:

Binary file not shown.

View File

@ -0,0 +1,7 @@
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(./Poppins.ttf) format('truetype');
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>platypush</title><script defer="defer" type="module" src="/static/js/chunk-vendors.5adf1720.js"></script><script defer="defer" type="module" src="/static/js/app.12b17001.js"></script><link href="/static/css/chunk-vendors.5cf89a0c.css" rel="stylesheet"><link href="/static/css/app.5028a669.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.6835b8d0.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.ab28664f.js" nomodule></script></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><link rel="stylesheet" href="/fonts/poppins.css"><title>platypush</title><script defer="defer" type="module" src="/static/js/chunk-vendors.95bedba1.js"></script><script defer="defer" type="module" src="/static/js/app.0ecd5641.js"></script><link href="/static/css/chunk-vendors.0fcd36f0.css" rel="stylesheet"><link href="/static/css/app.3b5b9cec.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.c27e0a41.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.602f8c67.js" nomodule></script></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[1595],{1595:function(e,t,n){n.r(t),n.d(t,{default:function(){return f}});var s=n(6252),o=n(3577),i={class:"date-time"},a=["textContent"],r=["textContent"];function u(e,t,n,u,h,w){return(0,s.wg)(),(0,s.iD)("div",i,[w._showDate?((0,s.wg)(),(0,s.iD)("div",{key:0,class:"date",textContent:(0,o.zw)(e.formatDate(e.now))},null,8,a)):(0,s.kq)("",!0),w._showTime?((0,s.wg)(),(0,s.iD)("div",{key:1,class:"time",textContent:(0,o.zw)(e.formatTime(e.now,w._showSeconds))},null,8,r)):(0,s.kq)("",!0)])}var h=n(2628),w={name:"DateTime",mixins:[h.Z],props:{showDate:{required:!1,default:!0},showTime:{required:!1,default:!0},showSeconds:{required:!1,default:!0}},computed:{_showTime:function(){return this.parseBoolean(this.showTime)},_showDate:function(){return this.parseBoolean(this.showDate)},_showSeconds:function(){return this.parseBoolean(this.showSeconds)}},data:function(){return{now:new Date}},methods:{refreshTime:function(){this.now=new Date}},mounted:function(){this.refreshTime(),setInterval(this.refreshTime,1e3)}},c=n(3744);const d=(0,c.Z)(w,[["render",u],["__scopeId","data-v-ca42eb9c"]]);var f=d}}]);
//# sourceMappingURL=1595-legacy.ddcdc704.js.map
"use strict";(self["webpackChunkplatypush"]=self["webpackChunkplatypush"]||[]).push([[1595],{1595:function(e,t,n){n.r(t),n.d(t,{default:function(){return f}});var s=n(6252),o=n(3577),i={class:"date-time"},a=["textContent"],r=["textContent"];function u(e,t,n,u,h,w){return(0,s.wg)(),(0,s.iD)("div",i,[w._showDate?((0,s.wg)(),(0,s.iD)("div",{key:0,class:"date",textContent:(0,o.zw)(e.formatDate(e.now))},null,8,a)):(0,s.kq)("",!0),w._showTime?((0,s.wg)(),(0,s.iD)("div",{key:1,class:"time",textContent:(0,o.zw)(e.formatTime(e.now,w._showSeconds))},null,8,r)):(0,s.kq)("",!0)])}var h=n(6813),w={name:"DateTime",mixins:[h.Z],props:{showDate:{required:!1,default:!0},showTime:{required:!1,default:!0},showSeconds:{required:!1,default:!0}},computed:{_showTime:function(){return this.parseBoolean(this.showTime)},_showDate:function(){return this.parseBoolean(this.showDate)},_showSeconds:function(){return this.parseBoolean(this.showSeconds)}},data:function(){return{now:new Date}},methods:{refreshTime:function(){this.now=new Date}},mounted:function(){this.refreshTime(),setInterval(this.refreshTime,1e3)}},c=n(3744);const d=(0,c.Z)(w,[["render",u],["__scopeId","data-v-ca42eb9c"]]);var f=d}}]);
//# sourceMappingURL=1595-legacy.69aea4ae.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"static/js/1595-legacy.69aea4ae.js","mappings":"0LACOA,MAAM,a,8EAAX,QAGM,MAHN,EAGM,CAF6C,EAAAC,YAAA,WAAjD,QAA8D,O,MAAzDD,MAAM,O,aAAO,QAAwB,EAAN,WAAC,EAAAE,OAArC,2BAC+D,EAAAC,YAAA,WAA/D,QAA4E,O,MAAvEH,MAAM,O,aAAO,QAAsC,EAApB,WAAC,EAAAE,IAAK,EAAAE,gBAA1C,4B,eAQJ,GACEC,KAAM,WACNC,OAAQ,CAACC,EAAA,GACTC,MAAO,CAELC,SAAU,CACRC,UAAU,EACVC,SAAS,GAIXC,SAAU,CACRF,UAAU,EACVC,SAAS,GAIXE,YAAa,CACXH,UAAU,EACVC,SAAS,IAIbG,SAAU,CACRX,UADQ,WAEN,OAAOY,KAAKC,aAAaD,KAAKH,SAC/B,EAEDX,UALQ,WAMN,OAAOc,KAAKC,aAAaD,KAAKN,SAC/B,EAEDL,aATQ,WAUN,OAAOW,KAAKC,aAAaD,KAAKF,YAC/B,GAGHI,KAAM,WACJ,MAAO,CACLf,IAAK,IAAIgB,KAEZ,EAEDC,QAAS,CACPC,YADO,WAELL,KAAKb,IAAM,IAAIgB,IAChB,GAGHG,QAAS,WACPN,KAAKK,cACLE,YAAYP,KAAKK,YAAa,IAC/B,G,UCxDH,MAAMG,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF,O","sources":["webpack://platypush/./src/components/widgets/DateTime/Index.vue","webpack://platypush/./src/components/widgets/DateTime/Index.vue?dfd6"],"sourcesContent":["<template>\n <div class=\"date-time\">\n <div class=\"date\" v-text=\"formatDate(now)\" v-if=\"_showDate\" />\n <div class=\"time\" v-text=\"formatTime(now, _showSeconds)\" v-if=\"_showTime\" />\n </div>\n</template>\n\n<script>\nimport Utils from \"@/Utils\";\n\n// Widget to show date and time\nexport default {\n name: 'DateTime',\n mixins: [Utils],\n props: {\n // If false then don't display the date.\n showDate: {\n required: false,\n default: true,\n },\n\n // If false then don't display the time.\n showTime: {\n required: false,\n default: true,\n },\n\n // If false then don't display the seconds.\n showSeconds: {\n required: false,\n default: true,\n },\n },\n\n computed: {\n _showTime() {\n return this.parseBoolean(this.showTime)\n },\n\n _showDate() {\n return this.parseBoolean(this.showDate)\n },\n\n _showSeconds() {\n return this.parseBoolean(this.showSeconds)\n },\n },\n\n data: function() {\n return {\n now: new Date(),\n };\n },\n\n methods: {\n refreshTime() {\n this.now = new Date()\n },\n },\n\n mounted: function() {\n this.refreshTime()\n setInterval(this.refreshTime, 1000)\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.date-time {\n .date {\n font-size: 1.3em;\n }\n\n .time {\n font-size: 2em;\n }\n}\n</style>\n","import { render } from \"./Index.vue?vue&type=template&id=ca42eb9c&scoped=true\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\n\nimport \"./Index.vue?vue&type=style&index=0&id=ca42eb9c&lang=scss&scoped=true\"\n\nimport exportComponent from \"/home/blacklight/git_tree/platypush/platypush/backend/http/webapp/node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-ca42eb9c\"]])\n\nexport default __exports__"],"names":["class","_showDate","now","_showTime","_showSeconds","name","mixins","Utils","props","showDate","required","default","showTime","showSeconds","computed","this","parseBoolean","data","Date","methods","refreshTime","mounted","setInterval","__exports__","render"],"sourceRoot":""}

View File

@ -1 +0,0 @@
{"version":3,"file":"static/js/1595-legacy.ddcdc704.js","mappings":"0LACOA,MAAM,a,8EAAX,QAGM,MAHN,EAGM,CAF6C,EAAAC,YAAA,WAAjD,QAA8D,O,MAAzDD,MAAM,O,aAAO,QAAwB,EAAN,WAAC,EAAAE,OAArC,2BAC+D,EAAAC,YAAA,WAA/D,QAA4E,O,MAAvEH,MAAM,O,aAAO,QAAsC,EAApB,WAAC,EAAAE,IAAK,EAAAE,gBAA1C,6B,cAQJ,GACEC,KAAM,WACNC,OAAQ,CAACC,EAAA,GACTC,MAAO,CAELC,SAAU,CACRC,UAAU,EACVC,SAAS,GAIXC,SAAU,CACRF,UAAU,EACVC,SAAS,GAIXE,YAAa,CACXH,UAAU,EACVC,SAAS,IAIbG,SAAU,CACRX,UADQ,WAEN,OAAOY,KAAKC,aAAaD,KAAKH,WAGhCX,UALQ,WAMN,OAAOc,KAAKC,aAAaD,KAAKN,WAGhCL,aATQ,WAUN,OAAOW,KAAKC,aAAaD,KAAKF,eAIlCI,KAAM,WACJ,MAAO,CACLf,IAAK,IAAIgB,OAIbC,QAAS,CACPC,YADO,WAELL,KAAKb,IAAM,IAAIgB,OAInBG,QAAS,WACPN,KAAKK,cACLE,YAAYP,KAAKK,YAAa,O,UCvDlC,MAAMG,GAA2B,OAAgB,EAAQ,CAAC,CAAC,SAASC,GAAQ,CAAC,YAAY,qBAEzF","sources":["webpack://platypush/./src/components/widgets/DateTime/Index.vue","webpack://platypush/./src/components/widgets/DateTime/Index.vue?dfd6"],"sourcesContent":["<template>\n <div class=\"date-time\">\n <div class=\"date\" v-text=\"formatDate(now)\" v-if=\"_showDate\" />\n <div class=\"time\" v-text=\"formatTime(now, _showSeconds)\" v-if=\"_showTime\" />\n </div>\n</template>\n\n<script>\nimport Utils from \"@/Utils\";\n\n// Widget to show date and time\nexport default {\n name: 'DateTime',\n mixins: [Utils],\n props: {\n // If false then don't display the date.\n showDate: {\n required: false,\n default: true,\n },\n\n // If false then don't display the time.\n showTime: {\n required: false,\n default: true,\n },\n\n // If false then don't display the seconds.\n showSeconds: {\n required: false,\n default: true,\n },\n },\n\n computed: {\n _showTime() {\n return this.parseBoolean(this.showTime)\n },\n\n _showDate() {\n return this.parseBoolean(this.showDate)\n },\n\n _showSeconds() {\n return this.parseBoolean(this.showSeconds)\n },\n },\n\n data: function() {\n return {\n now: new Date(),\n };\n },\n\n methods: {\n refreshTime() {\n this.now = new Date()\n },\n },\n\n mounted: function() {\n this.refreshTime()\n setInterval(this.refreshTime, 1000)\n },\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.date-time {\n .date {\n font-size: 1.3em;\n }\n\n .time {\n font-size: 2em;\n }\n}\n</style>\n","import { render } from \"./Index.vue?vue&type=template&id=ca42eb9c&scoped=true\"\nimport script from \"./Index.vue?vue&type=script&lang=js\"\nexport * from \"./Index.vue?vue&type=script&lang=js\"\n\nimport \"./Index.vue?vue&type=style&index=0&id=ca42eb9c&lang=scss&scoped=true\"\n\nimport exportComponent from \"/home/blacklight/git_tree/platypush/platypush/backend/http/webapp/node_modules/vue-loader/dist/exportHelper.js\"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__scopeId',\"data-v-ca42eb9c\"]])\n\nexport default __exports__"],"names":["class","_showDate","now","_showTime","_showSeconds","name","mixins","Utils","props","showDate","required","default","showTime","showSeconds","computed","this","parseBoolean","data","Date","methods","refreshTime","mounted","setInterval","__exports__","render"],"sourceRoot":""}

Some files were not shown because too many files have changed in this diff Show More