Compare commits

..

2 Commits

449 changed files with 9096 additions and 13074 deletions

View File

@ -3,94 +3,6 @@
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 `music.spotify.connect` backend to emulate a Spotify Connect receiver through Platypush.
## [0.21.1] - 2021-06-22
### Added
- Added `switchbot` plugin to interact with Switchbot devices over the cloud API instead of
directly accessing the device's Bluetooth interface.
- Added `marshmallow` dependency - it will be used from now own to dump and document schemas
and responses instead of the currently mixed approach with `Response` objects and plain
dictionaries and lists.
- Support for custom MQTT timeout on all the `zwavejs2mqtt` calls.
- Added generic joystick backend `backend.joystick.jstest` which uses `jstest` from the
standard `joystick` system package to read the state of joysticks not compatible with
`python-inputs`.
- Added PWM PCA9685 plugin.
- Added Linux native joystick plugin, ``backend.joystick.linux``, for the cases where
``python-inputs`` doesn't work and ``jstest`` is too slow.
### Changed
- `switch.switchbot` plugin renamed to `switchbot.bluetooth` plugin, while the new plugin
that uses the Switchbot API is simply named `switchbot`.
### Fixed
- More robust reconnection logic on the Pushbullet backend in case of websocket errors.
## [0.21.0] - 2021-05-06
### Added
- Support for custom PopcornTime API mirror/base URL.
- Full support for TV series search.
### Fixed
- Fixed torrent search (now using a different PopcornTime API mirror).
- Migrated SASS engine from `node-sass` (currently deprecated and broken on Node 16) to `sass`.
- Fixed alignment of Z-Wave UI header on Chrome/Webkit.
## [0.20.10] - 2021-04-28
### Fixed
- Fixed zwave/zwavejs2mqtt interoperability.
## [0.20.9] - 2021-04-12
### Added
- Added zwavejs2mqtt integration (see [#186](https://git.platypush.tech/platypush/platypush/-/issues/186).
### Fixed
- Major LINT fixes.
### Removed
- Removed unmaintained integrations: TorrentCast and Booking.com
## [0.20.8] - 2021-04-04
### Added
- Added `<Camera>` dashboard widget.
- Added support for custom dashboard widgets with customized (see https://git.platypush.tech/platypush/platypush/-/wikis/Backends#creating-custom-widgets).
- Added support for controls on `music.mpd` dashboard widget.
### Fixed
- Fixed zigbee2mqtt backend error in case of messages with empty payload (see [#184](https://git.platypush.tech/platypush/platypush/-/issues/184)).
- Fixed compatibility with all versions of websocket-client - versions >= 0.58.0 pass a `WebSocketApp` object as a first
argument to the callbacks, as well as versions < 0.54.0 do, but the versions in between don't pass this argument.
## [0.20.7] - 2021-03-26 ## [0.20.7] - 2021-03-26
### Fixed ### Fixed

View File

@ -7,8 +7,6 @@ Platypush
[![License](https://img.shields.io/github/license/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/blob/master/LICENSE.txt) [![License](https://img.shields.io/github/license/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/blob/master/LICENSE.txt)
[![Last Commit](https://img.shields.io/github/last-commit/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/commits/master/) [![Last Commit](https://img.shields.io/github/last-commit/BlackLight/platypush.svg)](https://git.platypush.tech/platypush/platypush/-/commits/master/)
[![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://git.platypush.tech/platypush/platypush/-/blob/master/CONTRIBUTING.md) [![Contributions](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://git.platypush.tech/platypush/platypush/-/blob/master/CONTRIBUTING.md)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/BlackLight/platypush.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/BlackLight/platypush/context:python)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/BlackLight/platypush.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/BlackLight/platypush/context:javascript)
- Recommended read: [**Getting started with Platypush**](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush). - Recommended read: [**Getting started with Platypush**](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush).

View File

@ -1,59 +0,0 @@
import importlib
import json
import os
import re
import sys
from typing import Union, List
from docutils import nodes
from docutils.parsers.rst import Directive
class SchemaDirective(Directive):
"""
Support for response/message schemas in the docs. Format: ``.. schema:: rel_path.SchemaClass(arg1=value1, ...)``,
where ``rel_path`` is the path of the schema relative to ``platypush/schemas``.
"""
has_content = True
_schema_regex = re.compile(r'^\s*(.+?)\s*(\((.+?)\))?\s*$')
_schemas_path = os.path.abspath(
os.path.join(
os.path.dirname(os.path.relpath(__file__)), '..', '..', '..', 'platypush', 'schemas'))
sys.path.insert(0, _schemas_path)
@staticmethod
def _get_field_value(field) -> str:
metadata = getattr(field, 'metadata', {})
return metadata.get('example', metadata.get('description', str(field.__class__.__name__).lower()))
def _parse_schema(self) -> Union[dict, List[dict]]:
m = self._schema_regex.match('\n'.join(self.content))
schema_module_name = '.'.join(['platypush.schemas', *(m.group(1).split('.')[:-1])])
schema_module = importlib.import_module(schema_module_name)
schema_class = getattr(schema_module, m.group(1).split('.')[-1])
schema_args = eval(f'dict({m.group(3)})')
schema = schema_class(**schema_args)
output = {
name: self._get_field_value(field)
for name, field in schema.fields.items()
if not field.load_only
}
return [output] if schema.many else output
def run(self):
content = json.dumps(self._parse_schema(), sort_keys=True, indent=2)
block = nodes.literal_block(content, content)
block['language'] = 'json'
return [block]
def setup(app):
app.add_directive('schema', SchemaDirective)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}

View File

@ -32,8 +32,6 @@ Backends
platypush/backend/http.poll.rst platypush/backend/http.poll.rst
platypush/backend/inotify.rst platypush/backend/inotify.rst
platypush/backend/joystick.rst platypush/backend/joystick.rst
platypush/backend/joystick.jstest.rst
platypush/backend/joystick.linux.rst
platypush/backend/kafka.rst platypush/backend/kafka.rst
platypush/backend/light.hue.rst platypush/backend/light.hue.rst
platypush/backend/linode.rst platypush/backend/linode.rst
@ -44,7 +42,6 @@ Backends
platypush/backend/music.mopidy.rst platypush/backend/music.mopidy.rst
platypush/backend/music.mpd.rst platypush/backend/music.mpd.rst
platypush/backend/music.snapcast.rst platypush/backend/music.snapcast.rst
platypush/backend/music.spotify.connect.rst
platypush/backend/nextcloud.rst platypush/backend/nextcloud.rst
platypush/backend/nfc.rst platypush/backend/nfc.rst
platypush/backend/nodered.rst platypush/backend/nodered.rst
@ -83,4 +80,3 @@ Backends
platypush/backend/wiimote.rst platypush/backend/wiimote.rst
platypush/backend/zigbee.mqtt.rst platypush/backend/zigbee.mqtt.rst
platypush/backend/zwave.rst platypush/backend/zwave.rst
platypush/backend/zwave.mqtt.rst

View File

@ -18,7 +18,6 @@ import sys
# import os # import os
# import sys # import sys
# sys.path.insert(0, os.path.abspath('.')) # sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath("./_ext"))
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
@ -51,7 +50,6 @@ extensions = [
'sphinx.ext.viewcode', 'sphinx.ext.viewcode',
'sphinx.ext.githubpages', 'sphinx.ext.githubpages',
'sphinx_rtd_theme', 'sphinx_rtd_theme',
'sphinx_marshmallow',
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
@ -136,11 +134,6 @@ html_theme_options = {
'title': 'Firefox Extension', 'title': 'Firefox Extension',
'internal': False, 'internal': False,
}, },
{
'href': 'https://f-droid.org/en/packages/tech.platypush.platypush/',
'title': 'Android App',
'internal': False,
},
], ],
} }

View File

@ -28,6 +28,7 @@ Events
platypush/events/gps.rst platypush/events/gps.rst
platypush/events/http.rst platypush/events/http.rst
platypush/events/http.hook.rst platypush/events/http.hook.rst
platypush/events/http.ota.booking.rst
platypush/events/http.rss.rst platypush/events/http.rss.rst
platypush/events/inotify.rst platypush/events/inotify.rst
platypush/events/joystick.rst platypush/events/joystick.rst

View File

@ -1,5 +0,0 @@
``platypush.backend.joystick.jstest``
=====================================
.. automodule:: platypush.backend.joystick.jstest
:members:

View File

@ -1,5 +0,0 @@
``platypush.backend.joystick.linux``
====================================
.. automodule:: platypush.backend.joystick.linux
:members:

View File

@ -1,5 +0,0 @@
``platypush.backend.music.spotify.connect``
===========================================
.. automodule:: platypush.backend.music.spotify.connect
:members:

View File

@ -1,5 +0,0 @@
``platypush.backend.zwave.mqtt``
================================
.. automodule:: platypush.backend.zwave.mqtt
:members:

View File

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

View File

@ -0,0 +1,5 @@
``platypush.plugins.http.request.ota.booking``
==============================================
.. automodule:: platypush.plugins.http.request.ota.booking
:members:

View File

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

View File

@ -0,0 +1,6 @@
``platypush.plugins.switch.switchbot``
======================================
.. automodule:: platypush.plugins.switch.switchbot
:members:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -61,6 +61,7 @@ Plugins
platypush/plugins/graphite.rst platypush/plugins/graphite.rst
platypush/plugins/homeseer.rst platypush/plugins/homeseer.rst
platypush/plugins/http.request.rst platypush/plugins/http.request.rst
platypush/plugins/http.request.ota.booking.rst
platypush/plugins/http.request.rss.rst platypush/plugins/http.request.rss.rst
platypush/plugins/http.webpage.rst platypush/plugins/http.webpage.rst
platypush/plugins/ifttt.rst platypush/plugins/ifttt.rst
@ -104,7 +105,6 @@ Plugins
platypush/plugins/ping.rst platypush/plugins/ping.rst
platypush/plugins/printer.cups.rst platypush/plugins/printer.cups.rst
platypush/plugins/pushbullet.rst platypush/plugins/pushbullet.rst
platypush/plugins/pwm.pca9685.rst
platypush/plugins/qrcode.rst platypush/plugins/qrcode.rst
platypush/plugins/redis.rst platypush/plugins/redis.rst
platypush/plugins/rtorrent.rst platypush/plugins/rtorrent.rst
@ -119,10 +119,9 @@ Plugins
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/switch.rst platypush/plugins/switch.rst
platypush/plugins/switch.switchbot.rst
platypush/plugins/switch.tplink.rst platypush/plugins/switch.tplink.rst
platypush/plugins/switch.wemo.rst platypush/plugins/switch.wemo.rst
platypush/plugins/switchbot.rst
platypush/plugins/switchbot.bluetooth.rst
platypush/plugins/system.rst platypush/plugins/system.rst
platypush/plugins/tcp.rst platypush/plugins/tcp.rst
platypush/plugins/tensorflow.rst platypush/plugins/tensorflow.rst
@ -138,6 +137,7 @@ Plugins
platypush/plugins/user.rst platypush/plugins/user.rst
platypush/plugins/utils.rst platypush/plugins/utils.rst
platypush/plugins/variable.rst platypush/plugins/variable.rst
platypush/plugins/video.torrentcast.rst
platypush/plugins/weather.rst platypush/plugins/weather.rst
platypush/plugins/weather.buienradar.rst platypush/plugins/weather.buienradar.rst
platypush/plugins/weather.darksky.rst platypush/plugins/weather.darksky.rst
@ -147,5 +147,3 @@ Plugins
platypush/plugins/zeroconf.rst platypush/plugins/zeroconf.rst
platypush/plugins/zigbee.mqtt.rst platypush/plugins/zigbee.mqtt.rst
platypush/plugins/zwave.rst platypush/plugins/zwave.rst
platypush/plugins/zwave._base.rst
platypush/plugins/zwave.mqtt.rst

View File

@ -24,7 +24,7 @@ from .utils import set_thread_name
__author__ = 'Fabio Manganiello <info@fabiomanganiello.com>' __author__ = 'Fabio Manganiello <info@fabiomanganiello.com>'
__version__ = '0.21.1' __version__ = '0.20.7'
logger = logging.getLogger('platypush') logger = logging.getLogger('platypush')

View File

@ -6,9 +6,10 @@
import logging import logging
import re import re
import socket import socket
import threading
import time import time
from threading import Thread, Event as ThreadEvent, get_ident from threading import Thread
from typing import Optional, Dict from typing import Optional, Dict
from platypush.bus import Bus from platypush.bus import Bus
@ -61,7 +62,7 @@ class Backend(Thread, EventGenerator):
self.poll_seconds = float(poll_seconds) if poll_seconds else None self.poll_seconds = float(poll_seconds) if poll_seconds else None
self.device_id = Config.get('device_id') self.device_id = Config.get('device_id')
self.thread_id = None self.thread_id = None
self._stop_event = ThreadEvent() self._stop_event = threading.Event()
self._kwargs = kwargs self._kwargs = kwargs
self.logger = logging.getLogger('platypush:backend:' + get_backend_name_by_class(self.__class__)) self.logger = logging.getLogger('platypush:backend:' + get_backend_name_by_class(self.__class__))
self.zeroconf = None self.zeroconf = None
@ -219,7 +220,7 @@ class Backend(Thread, EventGenerator):
def run(self): def run(self):
""" Starts the backend thread. To be implemented in the derived classes if the loop method isn't defined. """ """ Starts the backend thread. To be implemented in the derived classes if the loop method isn't defined. """
self.thread_id = get_ident() self.thread_id = threading.get_ident()
set_thread_name(self._thread_name) set_thread_name(self._thread_name)
if not callable(self.loop): if not callable(self.loop):
return return

View File

@ -65,11 +65,11 @@ class AdafruitIoBackend(Backend):
def on_message(self, msg): def on_message(self, msg):
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def _handler(client, feed, data): def _handler(client, feed, data):
# noinspection PyBroadException
try: try:
data = float(data) data = float(data)
except Exception as e: except:
self.logger.debug('Not a number: {}: {}'.format(data, e)) pass
self.bus.post(FeedUpdateEvent(feed=feed, data=data)) self.bus.post(FeedUpdateEvent(feed=feed, data=data))
return _handler return _handler

View File

@ -55,17 +55,16 @@ class Alarm:
self._runtime_snooze_interval = snooze_interval self._runtime_snooze_interval = snooze_interval
def get_next(self) -> float: def get_next(self) -> float:
now = datetime.datetime.now().replace(tzinfo=gettz()) # lgtm [py/call-to-non-callable] now = datetime.datetime.now().replace(tzinfo=gettz())
try: try:
cron = croniter.croniter(self.when, now) cron = croniter.croniter(self.when, now)
return cron.get_next() return cron.get_next()
except (AttributeError, croniter.CroniterBadCronError): except (AttributeError, croniter.CroniterBadCronError):
try: try:
timestamp = datetime.datetime.fromisoformat(self.when).replace( timestamp = datetime.datetime.fromisoformat(self.when).replace(tzinfo=gettz())
tzinfo=gettz()) # lgtm [py/call-to-non-callable]
except (TypeError, ValueError): except (TypeError, ValueError):
timestamp = (datetime.datetime.now().replace(tzinfo=gettz()) + # lgtm [py/call-to-non-callable] timestamp = (datetime.datetime.now().replace(tzinfo=gettz()) +
datetime.timedelta(seconds=int(self.when))) datetime.timedelta(seconds=int(self.when)))
return timestamp.timestamp() if timestamp >= now else None return timestamp.timestamp() if timestamp >= now else None

View File

@ -13,28 +13,25 @@ Bd addr are represented as standard python strings, e.g. "aa:bb:cc:dd:ee:ff".
import asyncio import asyncio
from enum import Enum from enum import Enum
from collections import namedtuple from collections import namedtuple
import time
import struct import struct
import itertools import itertools
class CreateConnectionChannelError(Enum): class CreateConnectionChannelError(Enum):
NoError = 0 NoError = 0
MaxPendingConnectionsReached = 1 MaxPendingConnectionsReached = 1
class ConnectionStatus(Enum): class ConnectionStatus(Enum):
Disconnected = 0 Disconnected = 0
Connected = 1 Connected = 1
Ready = 2 Ready = 2
class DisconnectReason(Enum): class DisconnectReason(Enum):
Unspecified = 0 Unspecified = 0
ConnectionEstablishmentFailed = 1 ConnectionEstablishmentFailed = 1
TimedOut = 2 TimedOut = 2
BondingKeysMismatch = 3 BondingKeysMismatch = 3
class RemovedReason(Enum): class RemovedReason(Enum):
RemovedByThisClient = 0 RemovedByThisClient = 0
ForceDisconnectedByThisClient = 1 ForceDisconnectedByThisClient = 1
@ -47,7 +44,6 @@ class RemovedReason(Enum):
CouldntLoadDevice = 7 CouldntLoadDevice = 7
class ClickType(Enum): class ClickType(Enum):
ButtonDown = 0 ButtonDown = 0
ButtonUp = 1 ButtonUp = 1
@ -56,24 +52,20 @@ class ClickType(Enum):
ButtonDoubleClick = 4 ButtonDoubleClick = 4
ButtonHold = 5 ButtonHold = 5
class BdAddrType(Enum): class BdAddrType(Enum):
PublicBdAddrType = 0 PublicBdAddrType = 0
RandomBdAddrType = 1 RandomBdAddrType = 1
class LatencyMode(Enum): class LatencyMode(Enum):
NormalLatency = 0 NormalLatency = 0
LowLatency = 1 LowLatency = 1
HighLatency = 2 HighLatency = 2
class BluetoothControllerState(Enum): class BluetoothControllerState(Enum):
Detached = 0 Detached = 0
Resetting = 1 Resetting = 1
Attached = 2 Attached = 2
class ScanWizardResult(Enum): class ScanWizardResult(Enum):
WizardSuccess = 0 WizardSuccess = 0
WizardCancelledByUser = 1 WizardCancelledByUser = 1
@ -83,7 +75,6 @@ class ScanWizardResult(Enum):
WizardInternetBackendError = 5 WizardInternetBackendError = 5
WizardInvalidData = 6 WizardInvalidData = 6
class ButtonScanner: class ButtonScanner:
"""ButtonScanner class. """ButtonScanner class.
@ -99,7 +90,6 @@ class ButtonScanner:
self._scan_id = next(ButtonScanner._cnt) self._scan_id = next(ButtonScanner._cnt)
self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None self.on_advertisement_packet = lambda scanner, bd_addr, name, rssi, is_private, already_verified: None
class ScanWizard: class ScanWizard:
"""ScanWizard class """ScanWizard class
@ -123,7 +113,6 @@ class ScanWizard:
self.on_button_connected = lambda scan_wizard, bd_addr, name: None self.on_button_connected = lambda scan_wizard, bd_addr, name: None
self.on_completed = lambda scan_wizard, result, bd_addr, name: None self.on_completed = lambda scan_wizard, result, bd_addr, name: None
class ButtonConnectionChannel: class ButtonConnectionChannel:
"""ButtonConnectionChannel class. """ButtonConnectionChannel class.
@ -175,9 +164,7 @@ class ButtonConnectionChannel:
self._latency_mode = latency_mode self._latency_mode = latency_mode
if not self._client._closed: if not self._client._closed:
self._client._send_command("CmdChangeModeParameters", self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
"auto_disconnect_time": self._auto_disconnect_time})
@property @property
def auto_disconnect_time(self): def auto_disconnect_time(self):
@ -191,10 +178,7 @@ class ButtonConnectionChannel:
self._auto_disconnect_time = auto_disconnect_time self._auto_disconnect_time = auto_disconnect_time
if not self._client._closed: if not self._client._closed:
self._client._send_command("CmdChangeModeParameters", self._client._send_command("CmdChangeModeParameters", {"conn_id": self._conn_id, "latency_mode": self._latency_mode, "auto_disconnect_time": self._auto_disconnect_time})
{"conn_id": self._conn_id, "latency_mode": self._latency_mode,
"auto_disconnect_time": self._auto_disconnect_time})
class FlicClient(asyncio.Protocol): class FlicClient(asyncio.Protocol):
"""FlicClient class. """FlicClient class.
@ -228,8 +212,7 @@ class FlicClient(asyncio.Protocol):
("EvtButtonSingleOrDoubleClick", "<IBBI", "conn_id click_type was_queued time_diff"), ("EvtButtonSingleOrDoubleClick", "<IBBI", "conn_id click_type was_queued time_diff"),
("EvtButtonSingleOrDoubleClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"), ("EvtButtonSingleOrDoubleClickOrHold", "<IBBI", "conn_id click_type was_queued time_diff"),
("EvtNewVerifiedButton", "<6s", "bd_addr"), ("EvtNewVerifiedButton", "<6s", "bd_addr"),
("EvtGetInfoResponse", "<B6sBBhBBH", ("EvtGetInfoResponse", "<B6sBBhBBH", "bluetooth_controller_state my_bd_addr my_bd_addr_type max_pending_connections max_concurrently_connected_buttons current_pending_connections currently_no_space_for_new_connection nb_verified_buttons"),
"bluetooth_controller_state my_bd_addr my_bd_addr_type max_pending_connections max_concurrently_connected_buttons current_pending_connections currently_no_space_for_new_connection nb_verified_buttons"),
("EvtNoSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"), ("EvtNoSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"), ("EvtGotSpaceForNewConnection", "<B", "max_concurrently_connected_buttons"),
("EvtBluetoothControllerStateChange", "<B", "state"), ("EvtBluetoothControllerStateChange", "<B", "state"),
@ -240,8 +223,8 @@ class FlicClient(asyncio.Protocol):
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"), ("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result") ("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
] ]
_EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _EVENTS)) _EVENT_STRUCTS = list(map(lambda x: None if x == None else struct.Struct(x[1]), _EVENTS))
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x is None else namedtuple(x[0], x[2]), _EVENTS)) _EVENT_NAMED_TUPLES = list(map(lambda x: None if x == None else namedtuple(x[0], x[2]), _EVENTS))
_COMMANDS = [ _COMMANDS = [
("CmdGetInfo", "", ""), ("CmdGetInfo", "", ""),
@ -261,11 +244,10 @@ class FlicClient(asyncio.Protocol):
_COMMAND_NAMED_TUPLES = list(map(lambda x: namedtuple(x[0], x[2]), _COMMANDS)) _COMMAND_NAMED_TUPLES = list(map(lambda x: namedtuple(x[0], x[2]), _COMMANDS))
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS)) _COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
@staticmethod
def _bdaddr_bytes_to_string(bdaddr_bytes): def _bdaddr_bytes_to_string(bdaddr_bytes):
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes))) return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
@staticmethod
def _bdaddr_string_to_bytes(bdaddr_string): def _bdaddr_string_to_bytes(bdaddr_string):
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":")))) return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
@ -291,6 +273,7 @@ class FlicClient(asyncio.Protocol):
if self.parent: if self.parent:
self.parent.register_protocol(self) self.parent.register_protocol(self)
def close(self): def close(self):
"""Closes the client. The handle_events() method will return.""" """Closes the client. The handle_events() method will return."""
if self._closed: if self._closed:
@ -359,9 +342,7 @@ class FlicClient(asyncio.Protocol):
channel._client = self channel._client = self
self._connection_channels[channel._conn_id] = channel self._connection_channels[channel._conn_id] = channel
self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr, self._send_command("CmdCreateConnectionChannel", {"conn_id": channel._conn_id, "bd_addr": channel.bd_addr, "latency_mode": channel._latency_mode, "auto_disconnect_time": channel._auto_disconnect_time})
"latency_mode": channel._latency_mode,
"auto_disconnect_time": channel._auto_disconnect_time})
def remove_connection_channel(self, channel): def remove_connection_channel(self, channel):
"""Remove a connection channel. """Remove a connection channel.
@ -403,6 +384,7 @@ class FlicClient(asyncio.Protocol):
""" """
self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr}) self._send_command("CmdGetButtonUUID", {"bd_addr": bd_addr})
def run_on_handle_events_thread(self, callback): def run_on_handle_events_thread(self, callback):
"""Run a function on the thread that handles the events.""" """Run a function on the thread that handles the events."""
if threading.get_ident() == self._handle_event_thread_ident: if threading.get_ident() == self._handle_event_thread_ident:
@ -417,7 +399,7 @@ class FlicClient(asyncio.Protocol):
items[key] = value.value items[key] = value.value
if "bd_addr" in items: if "bd_addr" in items:
items["bd_addr"] = FlicClient._bdaddr_string_to_bytes() items["bd_addr"] = FlicClient._bdaddr_string_to_bytes(items["bd_addr"])
opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name] opcode = FlicClient._COMMAND_NAME_TO_OPCODE[name]
data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items)) data_bytes = FlicClient._COMMAND_STRUCTS[opcode].pack(*FlicClient._COMMAND_NAMED_TUPLES[opcode](**items))
@ -433,7 +415,7 @@ class FlicClient(asyncio.Protocol):
return return
opcode = data[0] opcode = data[0]
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] is None: if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] == None:
return return
event_name = FlicClient._EVENTS[opcode][0] event_name = FlicClient._EVENTS[opcode][0]
@ -442,7 +424,7 @@ class FlicClient(asyncio.Protocol):
# Process some kind of items whose data type is not supported by struct # Process some kind of items whose data type is not supported by struct
if "bd_addr" in items: if "bd_addr" in items:
items["bd_addr"] = FlicClient._bdaddr_bytes_to_string() items["bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["bd_addr"])
if "name" in items: if "name" in items:
items["name"] = items["name"].decode("utf-8") items["name"] = items["name"].decode("utf-8")
@ -463,14 +445,13 @@ class FlicClient(asyncio.Protocol):
if event_name == "EvtGetInfoResponse": if event_name == "EvtGetInfoResponse":
items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"]) items["bluetooth_controller_state"] = BluetoothControllerState(items["bluetooth_controller_state"])
items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string() items["my_bd_addr"] = FlicClient._bdaddr_bytes_to_string(items["my_bd_addr"])
items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"]) items["my_bd_addr_type"] = BdAddrType(items["my_bd_addr_type"])
items["bd_addr_of_verified_buttons"] = [] items["bd_addr_of_verified_buttons"] = []
pos = FlicClient._EVENT_STRUCTS[opcode].size pos = FlicClient._EVENT_STRUCTS[opcode].size
for i in range(items["nb_verified_buttons"]): for i in range(items["nb_verified_buttons"]):
items["bd_addr_of_verified_buttons"].append( items["bd_addr_of_verified_buttons"].append(FlicClient._bdaddr_bytes_to_string(data[1 + pos : 1 + pos + 6]))
FlicClient._bdaddr_bytes_to_string())
pos += 6 pos += 6
if event_name == "EvtBluetoothControllerStateChange": if event_name == "EvtBluetoothControllerStateChange":
@ -488,8 +469,7 @@ class FlicClient(asyncio.Protocol):
if event_name == "EvtAdvertisementPacket": if event_name == "EvtAdvertisementPacket":
scanner = self._scanners.get(items["scan_id"]) scanner = self._scanners.get(items["scan_id"])
if scanner is not None: if scanner is not None:
scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"], scanner.on_advertisement_packet(scanner, items["bd_addr"], items["name"], items["rssi"], items["is_private"], items["already_verified"])
items["is_private"], items["already_verified"])
if event_name == "EvtCreateConnectionChannelResponse": if event_name == "EvtCreateConnectionChannelResponse":
channel = self._connection_channels[items["conn_id"]] channel = self._connection_channels[items["conn_id"]]
@ -514,12 +494,10 @@ class FlicClient(asyncio.Protocol):
channel.on_button_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"]) channel.on_button_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
if event_name == "EvtButtonSingleOrDoubleClick": if event_name == "EvtButtonSingleOrDoubleClick":
channel = self._connection_channels[items["conn_id"]] channel = self._connection_channels[items["conn_id"]]
channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"], channel.on_button_single_or_double_click(channel, items["click_type"], items["was_queued"], items["time_diff"])
items["time_diff"])
if event_name == "EvtButtonSingleOrDoubleClickOrHold": if event_name == "EvtButtonSingleOrDoubleClickOrHold":
channel = self._connection_channels[items["conn_id"]] channel = self._connection_channels[items["conn_id"]]
channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"], channel.on_button_single_or_double_click_or_hold(channel, items["click_type"], items["was_queued"], items["time_diff"])
items["time_diff"])
if event_name == "EvtNewVerifiedButton": if event_name == "EvtNewVerifiedButton":
self.on_new_verified_button(items["bd_addr"]) self.on_new_verified_button(items["bd_addr"])
@ -558,6 +536,7 @@ class FlicClient(asyncio.Protocol):
del self._scan_wizards[items["scan_wizard_id"]] del self._scan_wizards[items["scan_wizard_id"]]
scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name) scan_wizard.on_completed(scan_wizard, items["result"], scan_wizard._bd_addr, scan_wizard._name)
def data_received(self,data): def data_received(self,data):
cdata=self.buffer+data cdata=self.buffer+data
self.buffer=b"" self.buffer=b""
@ -571,3 +550,5 @@ class FlicClient(asyncio.Protocol):
if len(cdata): if len(cdata):
self.buffer=cdata #unlikely to happen but..... self.buffer=cdata #unlikely to happen but.....
break break

View File

@ -229,8 +229,8 @@ class FlicClient:
("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"), ("EvtScanWizardButtonConnected", "<I", "scan_wizard_id"),
("EvtScanWizardCompleted", "<IB", "scan_wizard_id result") ("EvtScanWizardCompleted", "<IB", "scan_wizard_id result")
] ]
_EVENT_STRUCTS = list(map(lambda x: None if x is None else struct.Struct(x[1]), _EVENTS)) _EVENT_STRUCTS = list(map(lambda x: None if x == None else struct.Struct(x[1]), _EVENTS))
_EVENT_NAMED_TUPLES = list(map(lambda x: None if x is None else namedtuple(x[0], x[2]), _EVENTS)) _EVENT_NAMED_TUPLES = list(map(lambda x: None if x == None else namedtuple(x[0], x[2]), _EVENTS))
_COMMANDS = [ _COMMANDS = [
("CmdGetInfo", "", ""), ("CmdGetInfo", "", ""),
@ -250,11 +250,9 @@ class FlicClient:
_COMMAND_NAMED_TUPLES = list(map(lambda x: namedtuple(x[0], x[2]), _COMMANDS)) _COMMAND_NAMED_TUPLES = list(map(lambda x: namedtuple(x[0], x[2]), _COMMANDS))
_COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS)) _COMMAND_NAME_TO_OPCODE = dict((x[0], i) for i, x in enumerate(_COMMANDS))
@staticmethod
def _bdaddr_bytes_to_string(bdaddr_bytes): def _bdaddr_bytes_to_string(bdaddr_bytes):
return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes))) return ":".join(map(lambda x: "%02x" % x, reversed(bdaddr_bytes)))
@staticmethod
def _bdaddr_string_to_bytes(bdaddr_string): def _bdaddr_string_to_bytes(bdaddr_string):
return bytearray.fromhex("".join(reversed(bdaddr_string.split(":")))) return bytearray.fromhex("".join(reversed(bdaddr_string.split(":"))))
@ -440,7 +438,7 @@ class FlicClient:
return return
opcode = data[0] opcode = data[0]
if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] is None: if opcode >= len(FlicClient._EVENTS) or FlicClient._EVENTS[opcode] == None:
return return
event_name = FlicClient._EVENTS[opcode][0] event_name = FlicClient._EVENTS[opcode][0]

View File

@ -34,7 +34,7 @@ class CameraPiBackend(Backend):
return self.value == other return self.value == other
# noinspection PyUnresolvedReferences,PyPackageRequirements # noinspection PyUnresolvedReferences,PyPackageRequirements
def __init__(self, listen_port, bind_address='0.0.0.0', x_resolution=640, y_resolution=480, def __init__(self, listen_port, x_resolution=640, y_resolution=480,
redis_queue='platypush/camera/pi', redis_queue='platypush/camera/pi',
start_recording_on_startup=True, start_recording_on_startup=True,
framerate=24, hflip=False, vflip=False, framerate=24, hflip=False, vflip=False,
@ -49,17 +49,13 @@ class CameraPiBackend(Backend):
:param listen_port: Port where the camera process will provide the video output while recording :param listen_port: Port where the camera process will provide the video output while recording
:type listen_port: int :type listen_port: int
:param bind_address: Bind address (default: 0.0.0.0).
:type bind_address: str
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
self.bind_address = bind_address
self.listen_port = listen_port self.listen_port = listen_port
self.server_socket = socket.socket() self.server_socket = socket.socket()
self.server_socket.bind((self.bind_address, self.listen_port)) self.server_socket.bind(('0.0.0.0', self.listen_port))
self.server_socket.listen(0) self.server_socket.listen(0)
import picamera import picamera
@ -138,13 +134,13 @@ class CameraPiBackend(Backend):
self.logger.info('Client closed connection') self.logger.info('Client closed connection')
try: try:
self.stop_recording() self.stop_recording()
except Exception as e: except:
self.logger.warning('Could not stop recording: {}'.format(str(e))) pass
try: try:
connection.close() connection.close()
except Exception as e: except:
self.logger.warning('Could not close connection: {}'.format(str(e))) pass
self.send_camera_action(self.CameraAction.START_RECORDING) self.send_camera_action(self.CameraAction.START_RECORDING)

View File

@ -51,10 +51,11 @@ class GooglePubsubBackend(Backend):
def _message_callback(self, topic): def _message_callback(self, topic):
def callback(msg): def callback(msg):
data = msg.data.decode() data = msg.data.decode()
# noinspection PyBroadException
try: try:
data = json.loads(data) data = json.loads(data)
except Exception as e: except:
self.logger.debug('Not a valid JSON: {}: {}'.format(data, str(e))) pass
msg.ack() msg.ack()
self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data)) self.bus.post(GooglePubsubMessageEvent(topic=topic, msg=data))

View File

@ -31,8 +31,8 @@ def _hook(hook_name):
# noinspection PyBroadException # noinspection PyBroadException
try: try:
event_args['data'] = json.loads(event_args['data']) event_args['data'] = json.loads(event_args['data'])
except Exception as e: except:
logger().warning('Not a valid JSON string: {}: {}'.format(event_args['data'], str(e))) pass
event = WebhookEvent(**event_args) event = WebhookEvent(**event_args)

View File

@ -31,7 +31,7 @@ def login():
if session_token: if session_token:
user, session = user_manager.authenticate_user_session(session_token) user, session = user_manager.authenticate_user_session(session_token)
if user: if user:
return redirect(redirect_page, 302) # lgtm [py/url-redirection] return redirect(redirect_page, 302)
if request.form: if request.form:
username = request.form.get('username') username = request.form.get('username')
@ -44,7 +44,7 @@ def login():
expires_at=expires) expires_at=expires)
if session: if session:
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection] redirect_target = redirect(redirect_page, 302)
response = make_response(redirect_target) response = make_response(redirect_target)
response.set_cookie('session_token', session.session_token, expires=expires) response.set_cookie('session_token', session.session_token, expires=expires)
return response return response

View File

@ -25,7 +25,7 @@ def logout():
if not user: if not user:
return abort(403, 'Invalid session token') return abort(403, 'Invalid session token')
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection] redirect_target = redirect(redirect_page, 302)
response = make_response(redirect_target) response = make_response(redirect_target)
response.set_cookie('session_token', '', expires=0) response.set_cookie('session_token', '', expires=0)
return response return response

View File

@ -45,12 +45,14 @@ def get_args(kwargs):
if k == 'resolution': if k == 'resolution':
v = json.loads('[{}]'.format(v)) v = json.loads('[{}]'.format(v))
else: else:
# noinspection PyBroadException
try: try:
v = int(v) v = int(v)
except (ValueError, TypeError): except:
# noinspection PyBroadException
try: try:
v = float(v) v = float(v)
except (ValueError, TypeError): except:
pass pass
kwargs[k] = v kwargs[k] = v

View File

@ -32,8 +32,8 @@ def add_media():
args = {} args = {}
try: try:
args = json.loads(request.data.decode('utf-8')) args = json.loads(request.data.decode('utf-8'))
except Exception as e: except:
abort(400, 'Invalid JSON request: {}'.format(str(e))) abort(400, 'Invalid JSON request')
source = args.get('source') source = args.get('source')
if not source: if not source:

View File

@ -40,7 +40,7 @@ def audio_feed(device, fifo, sample_rate, blocksize, latency, channels):
channels=channels) channels=channels)
try: try:
with open(fifo, 'rb') as f: # lgtm [py/path-injection] with open(fifo, 'rb') as f:
send_header = True send_header = True
while True: while True:

View File

@ -31,10 +31,10 @@ def register():
if session_token: if session_token:
user, session = user_manager.authenticate_user_session(session_token) user, session = user_manager.authenticate_user_session(session_token)
if user: if user:
return redirect(redirect_page, 302) # lgtm [py/url-redirection] return redirect(redirect_page, 302)
if user_manager.get_user_count() > 0: if user_manager.get_user_count() > 0:
return redirect('/login?redirect=' + redirect_page, 302) # lgtm [py/url-redirection] return redirect('/login?redirect=' + redirect_page, 302)
if request.form: if request.form:
username = request.form.get('username') username = request.form.get('username')
@ -49,7 +49,7 @@ def register():
if not remember else None) if not remember else None)
if session: if session:
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection] redirect_target = redirect(redirect_page, 302)
response = make_response(redirect_target) response = make_response(redirect_target)
response.set_cookie('session_token', session.session_token) response.set_cookie('session_token', session.session_token)
return response return response

View File

@ -123,8 +123,7 @@ def _authenticate_token():
try: try:
user_manager.validate_jwt_token(user_token) user_manager.validate_jwt_token(user_token)
return True return True
except Exception as e: except:
logger().debug(str(e))
return token and user_token == token return token and user_token == token

View File

@ -78,11 +78,11 @@ class HttpRequest(object):
def get_new_items(self, response): def get_new_items(self, response):
""" Gets new items out of a response """ """ Gets new items out of a response """
raise NotImplementedError("get_new_items must be implemented in a derived class") raise ("get_new_items must be implemented in a derived class")
def __iter__(self): def __iter__(self):
for (key, value) in self.request_args.items(): for (key, value) in self.request_args.items():
yield key, value yield (key, value)
class JsonHttpRequest(HttpRequest): class JsonHttpRequest(HttpRequest):
@ -96,7 +96,7 @@ class JsonHttpRequest(HttpRequest):
new_entries = [] new_entries = []
if self.path: if self.path:
m = re.match(r'\${\s*(.*)\s*}', self.path) m = re.match('\$\{\s*(.*)\s*\}', self.path)
response = eval(m.group(1)) response = eval(m.group(1))
for entry in response: for entry in response:

View File

@ -238,15 +238,15 @@ class RssUpdates(HttpRequest):
with open(digest_filename, 'w', encoding='utf-8') as f: with open(digest_filename, 'w', encoding='utf-8') as f:
f.write(content) f.write(content)
elif self.digest_format == 'pdf': elif self.digest_format == 'pdf':
from weasyprint import HTML, CSS import weasyprint
from weasyprint.fonts import FontConfiguration from weasyprint.fonts import FontConfiguration
body_style = 'body { ' + self.body_style + ' }' body_style = 'body { {body_style} }'.format(body_style=self.body_style)
font_config = FontConfiguration() font_config = FontConfiguration()
css = [CSS('https://fonts.googleapis.com/css?family=Merriweather'), css = [weasyprint.CSS('https://fonts.googleapis.com/css?family=Merriweather'),
CSS(string=body_style, font_config=font_config)] weasyprint.CSS(string=body_style, font_config=font_config)]
HTML(string=content).write_pdf(digest_filename, stylesheets=css) weasyprint.HTML(string=content).write_pdf(digest_filename, stylesheets=css)
else: else:
raise RuntimeError('Unsupported format: {}. Supported formats: ' + raise RuntimeError('Unsupported format: {}. Supported formats: ' +
'html or pdf'.format(self.digest_format)) 'html or pdf'.format(self.digest_format))

View File

@ -50,6 +50,8 @@ class HttpUtils(object):
for name, resource_path in resource_dirs.items(): for name, resource_path in resource_dirs.items():
resource_path = os.path.abspath(os.path.expanduser(resource_path)) resource_path = os.path.abspath(os.path.expanduser(resource_path))
if directory.startswith(resource_path): if directory.startswith(resource_path):
subdir = re.sub('^{}(.*)$'.format(resource_path),
'\\1', directory)
uri = '/resources/' + name uri = '/resources/' + name
break break
@ -94,7 +96,6 @@ class HttpUtils(object):
@classmethod @classmethod
def find_templates_in_dir(cls, directory): def find_templates_in_dir(cls, directory):
# noinspection PyTypeChecker
return [ return [
os.path.join(directory, file) os.path.join(directory, file)
for root, path, files in os.walk(os.path.abspath(os.path.join(template_folder, directory))) for root, path, files in os.walk(os.path.abspath(os.path.join(template_folder, directory)))

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

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

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