Added Join plugin to control remote Android devices
This commit is contained in:
parent
0f0f8f8a94
commit
e3d44b56dd
3 changed files with 432 additions and 0 deletions
6
docs/source/platypush/plugins/mobile.join.__init__.rst
Normal file
6
docs/source/platypush/plugins/mobile.join.__init__.rst
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
``platypush.plugins.mobile.join.__init__``
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.mobile.join.__init__
|
||||||
|
:members:
|
||||||
|
|
|
@ -58,6 +58,7 @@ Plugins
|
||||||
platypush/plugins/media.webtorrent.rst
|
platypush/plugins/media.webtorrent.rst
|
||||||
platypush/plugins/midi.rst
|
platypush/plugins/midi.rst
|
||||||
platypush/plugins/ml.cv.rst
|
platypush/plugins/ml.cv.rst
|
||||||
|
platypush/plugins/mobile.join.rst
|
||||||
platypush/plugins/mqtt.rst
|
platypush/plugins/mqtt.rst
|
||||||
platypush/plugins/music.mpd.rst
|
platypush/plugins/music.mpd.rst
|
||||||
platypush/plugins/music.snapcast.rst
|
platypush/plugins/music.snapcast.rst
|
||||||
|
|
425
platypush/plugins/mobile/join/__init__.py
Normal file
425
platypush/plugins/mobile/join/__init__.py
Normal file
|
@ -0,0 +1,425 @@
|
||||||
|
import enum
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from platypush.plugins import Plugin, action
|
||||||
|
|
||||||
|
|
||||||
|
class InterruptionFilterPolicy(enum.Enum):
|
||||||
|
ALLOW_ALL = 1
|
||||||
|
PRIORITY_ONLY = 2
|
||||||
|
BLOCK_ALL = 3
|
||||||
|
ALARM_ONLY = 4
|
||||||
|
|
||||||
|
|
||||||
|
class MobileJoinPlugin(Plugin):
|
||||||
|
"""
|
||||||
|
Control mobile devices and other devices linked to Join (https://joaoapps.com/join/api/).
|
||||||
|
It requires you to have either the Join app installed on an Android device or the Join
|
||||||
|
browser extension installed.
|
||||||
|
|
||||||
|
The ``device`` parameter in the actions can be:
|
||||||
|
|
||||||
|
- A device ID or name
|
||||||
|
- A list or comma-separated list of device IDs/names
|
||||||
|
- A group name or a list of group names
|
||||||
|
|
||||||
|
Supported groups:
|
||||||
|
|
||||||
|
- group.all
|
||||||
|
- group.android
|
||||||
|
- group.windows10
|
||||||
|
- group.phone
|
||||||
|
- group.tablet
|
||||||
|
- group.pc
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_base_url = 'https://joinjoaomgcd.appspot.com/_ah/api'
|
||||||
|
_push_url = '{}/messaging/v1/sendPush'.format(_base_url)
|
||||||
|
_devices_url = '{}/registration/v1/listDevices'.format(_base_url)
|
||||||
|
_groups = {
|
||||||
|
'group.all',
|
||||||
|
'group.android',
|
||||||
|
'group.windows10',
|
||||||
|
'group.phone',
|
||||||
|
'group.tablet',
|
||||||
|
'group.pc',
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, api_key: str, **kwargs):
|
||||||
|
"""
|
||||||
|
:param api_key: Join API key. Get your at https://joinjoaomgcd.appspot.com/.
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self._api_key = api_key
|
||||||
|
self._devices = {}
|
||||||
|
self._devices_by_id = {}
|
||||||
|
self._devices_by_name = {}
|
||||||
|
self._devices_lock = threading.RLock()
|
||||||
|
|
||||||
|
def _send_request(self, url, device=None, params: dict = None, **kwargs):
|
||||||
|
if not params:
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
params['apikey'] = self._api_key
|
||||||
|
if device:
|
||||||
|
params['deviceIds'] = self._get_device_ids(device)
|
||||||
|
|
||||||
|
response = requests.get(url, params=params, **kwargs)
|
||||||
|
if not response.ok:
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
response = response.json()
|
||||||
|
if response.get('userAuthError'):
|
||||||
|
raise PermissionError('Invalid api_key provided')
|
||||||
|
|
||||||
|
del response['userAuthError']
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _init_devices(self):
|
||||||
|
with self._devices_lock:
|
||||||
|
if not self._devices:
|
||||||
|
self._devices = self.get_devices().output
|
||||||
|
self._devices_by_id = {dev['id']: dev for dev in self._devices}
|
||||||
|
self._devices_by_name = {dev['name']: dev for dev in self._devices}
|
||||||
|
|
||||||
|
def _get_device_ids(self, device):
|
||||||
|
devices = [dev.strip() for dev in device.split(',')] \
|
||||||
|
if isinstance(device, str) else device
|
||||||
|
assert isinstance(devices, list)
|
||||||
|
|
||||||
|
has_unknown_devices = True
|
||||||
|
cache_refreshed = False
|
||||||
|
device_ids = []
|
||||||
|
|
||||||
|
while has_unknown_devices:
|
||||||
|
has_unknown_devices = False
|
||||||
|
|
||||||
|
for dev in devices:
|
||||||
|
if dev in self._devices_by_id:
|
||||||
|
device_ids.append(self._devices_by_id[dev]['id'])
|
||||||
|
elif dev in self._devices_by_name:
|
||||||
|
device_ids.append(self._devices_by_name[dev]['id'])
|
||||||
|
elif dev in self._groups:
|
||||||
|
device_ids.append(dev)
|
||||||
|
else:
|
||||||
|
has_unknown_devices = True
|
||||||
|
|
||||||
|
if has_unknown_devices:
|
||||||
|
if cache_refreshed:
|
||||||
|
raise KeyError('No such device: {}'.format(device))
|
||||||
|
|
||||||
|
self._init_devices()
|
||||||
|
cache_refreshed = True
|
||||||
|
|
||||||
|
return device_ids
|
||||||
|
|
||||||
|
@action
|
||||||
|
def push(self, device, text=None, url=None, file=None):
|
||||||
|
"""
|
||||||
|
Push a URL or file to one or more devices
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param text: Optional description text for the URL or file
|
||||||
|
:param url: URL to be pushed
|
||||||
|
:param file: A publicly accessible URL of a file. You can also send the url of a file on your personal
|
||||||
|
Google Drive
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert url or file
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if text:
|
||||||
|
params['text'] = text
|
||||||
|
if url:
|
||||||
|
params['url'] = url
|
||||||
|
if file:
|
||||||
|
params['file'] = file
|
||||||
|
|
||||||
|
return self._send_request(self._push_url, device=device, params=params)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set_clipboard(self, device, text):
|
||||||
|
"""
|
||||||
|
Write to the clipboard of a device
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param text: Text to be set
|
||||||
|
"""
|
||||||
|
return self._send_request(self._push_url, device=device, params={'clipboard': text})
|
||||||
|
|
||||||
|
@action
|
||||||
|
def send_sms(self, device, text: str, number: str = None, contact_name: str = None):
|
||||||
|
"""
|
||||||
|
Send an sms through a mobile device connected to Join
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param text: Text to be sent
|
||||||
|
:param number: Phone number
|
||||||
|
:param contact_name: Alternatively to the phone number, you can specify a contact name
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert (number or contact_name) and not (number and contact_name)
|
||||||
|
params = {'smstext': text}
|
||||||
|
|
||||||
|
if number:
|
||||||
|
params['smsnumber'] = number
|
||||||
|
else:
|
||||||
|
params['smscontactname'] = contact_name
|
||||||
|
|
||||||
|
return self._send_request(self._push_url, device=device, params=params)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def send_mms(self, device, file: str = None, text: str = None, subject: str = '',
|
||||||
|
number: str = None, contact_name: str = None, urgent: bool = False):
|
||||||
|
"""
|
||||||
|
Send an MMS through a mobile device connected to Join
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param text: Text to be sent
|
||||||
|
:param number: Phone number
|
||||||
|
:param contact_name: Alternatively to the phone number, you can specify a contact name
|
||||||
|
:param file: File attached to the message. Must be a local (to the phone) file or a publicly accessible URL
|
||||||
|
:param subject: MMS subject
|
||||||
|
:param urgent: Set to True if this is an urgent MMS. This will make the sent message be an MMS instead of an SMS
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert (number or contact_name) and not (number and contact_name)
|
||||||
|
params = {
|
||||||
|
'mmssubject': subject,
|
||||||
|
'mmsurgent': int(urgent),
|
||||||
|
}
|
||||||
|
|
||||||
|
if number:
|
||||||
|
params['smsnumber'] = number
|
||||||
|
else:
|
||||||
|
params['smscontactname'] = contact_name
|
||||||
|
if file:
|
||||||
|
params['mmsfile'] = file
|
||||||
|
if text:
|
||||||
|
params['smstext'] = text
|
||||||
|
|
||||||
|
return self._send_request(self._push_url, device=device, params=params)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def call_number(self, device, number: str):
|
||||||
|
"""
|
||||||
|
Call a phone number through a mobile device connected to Join
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param number: Phone number
|
||||||
|
"""
|
||||||
|
return self._send_request(self._push_url, device=device, params={'callnumber': number})
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set_wallpaper(self, device, wallpaper: str):
|
||||||
|
"""
|
||||||
|
Set the wallpaper on a device connected to Join
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param wallpaper: A publicly accessible URL of an image file. Will set the wallpaper on the receiving device
|
||||||
|
"""
|
||||||
|
return self._send_request(self._push_url, device=device, params={'wallpaper': wallpaper})
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set_lock_wallpaper(self, device, wallpaper: str):
|
||||||
|
"""
|
||||||
|
Set the lock wallpaper on a device connected to Join
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param wallpaper: A publicly accessible URL of an image file. Will set the lockscreen wallpaper on the receiving
|
||||||
|
device if the device has Android 7 or above
|
||||||
|
"""
|
||||||
|
return self._send_request(self._push_url, device=device, params={'wallpaper': wallpaper})
|
||||||
|
|
||||||
|
@action
|
||||||
|
def find(self, device):
|
||||||
|
"""
|
||||||
|
Make a device ring loudly
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
"""
|
||||||
|
return self._send_request(self._push_url, device=device, params={'find': True})
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set_media_volume(self, device, volume: float):
|
||||||
|
"""
|
||||||
|
Set media volume
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param volume: Volume
|
||||||
|
"""
|
||||||
|
return self._send_request(self._push_url, device=device, params={'mediaVolume': volume})
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set_ring_volume(self, device, volume: float):
|
||||||
|
"""
|
||||||
|
Set ring volume
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param volume: Volume
|
||||||
|
"""
|
||||||
|
return self._send_request(self._push_url, device=device, params={'ringVolume': volume})
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set_alarm_volume(self, device, volume: float):
|
||||||
|
"""
|
||||||
|
Set alarm volume
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param volume: Volume
|
||||||
|
"""
|
||||||
|
return self._send_request(self._push_url, device=device, params={'alarmVolume': volume})
|
||||||
|
|
||||||
|
@action
|
||||||
|
def set_interruption_filter(self, device, policy: str):
|
||||||
|
"""
|
||||||
|
Set interruption filter on one or more devices
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param policy: Possible values:
|
||||||
|
|
||||||
|
- 'allow_all': Allow all notifications
|
||||||
|
- 'priority_only': Allow only priority notifications and calls
|
||||||
|
- 'alarm_only': Allow only alarm-related interruptions
|
||||||
|
- 'block_all': Do not allow any interruptions
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
policy = getattr(InterruptionFilterPolicy, policy.upper())
|
||||||
|
except AttributeError:
|
||||||
|
raise AttributeError('Invalid policy: {}. Supported values: {}'.format(
|
||||||
|
policy, [i.name for i in InterruptionFilterPolicy]
|
||||||
|
))
|
||||||
|
|
||||||
|
return self._send_request(self._push_url, device=device, params={'interruptionFilter': policy})
|
||||||
|
|
||||||
|
@action
|
||||||
|
def say(self, device, text: str, language: str = None):
|
||||||
|
"""
|
||||||
|
Say some text through a device's TTS engine
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param text: Text to say
|
||||||
|
:param language: Language code
|
||||||
|
"""
|
||||||
|
params = {'say': text}
|
||||||
|
if language:
|
||||||
|
params['language'] = language
|
||||||
|
|
||||||
|
return self._send_request(self._push_url, device=device, params=params)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def launch_app(self, device, name: str = None, package: str = None):
|
||||||
|
"""
|
||||||
|
Launch an app on a device
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param name: Application name
|
||||||
|
:param package: Alternatively to the application name, you can also specify the application
|
||||||
|
package name. You can check the package name for an app by going to its Google Play page and checking
|
||||||
|
the end of the URL. Example: for YouTube this is the URL
|
||||||
|
(https://play.google.com/store/apps/details?id=com.google.android.youtube) and this is the package name
|
||||||
|
(com.google.android.youtube)
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert (name or package) and not (name and package)
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
if name:
|
||||||
|
params['app'] = name
|
||||||
|
else:
|
||||||
|
params['appPackage'] = package
|
||||||
|
|
||||||
|
return self._send_request(self._push_url, device=device, params=params)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def send_notification(self, device, title: str = None, text: str = None, url: str = None, file: str = None,
|
||||||
|
icon: str = None, small_icon: str = None, priority: int = 2, vibration_pattern=None,
|
||||||
|
dismiss_on_touch: bool = False, image: str = None, group: str = None,
|
||||||
|
sound: str = None, actions=None):
|
||||||
|
"""
|
||||||
|
Send a notification to a device
|
||||||
|
|
||||||
|
:param device: Device ID or name, or list of device IDs/names, or group name(s)
|
||||||
|
:param title: Notification title
|
||||||
|
:param text: Notification text
|
||||||
|
:param url: URL to be opened on touch
|
||||||
|
:param file: A publicly accessible URL of a file that will be opened or downloaded on touch. You can also
|
||||||
|
send the url of a file on your personal Google Drive.
|
||||||
|
:param icon: If a notification is created on the receiving device and this is set, then it'll be used as
|
||||||
|
the notification’s icon. If this image has transparency, it’ll also be used as the status bar icon is
|
||||||
|
smallicon is not set. It’ll also be used to tint the notification with its dominating color
|
||||||
|
:param small_icon: If a notification is created on the receiving device and this is set, then it’ll be used as
|
||||||
|
the notification’s status bar icon
|
||||||
|
:param priority: Control how your notification is displayed: lower priority notifications are usually
|
||||||
|
displayed lower in the notification list. Values from -2 (lowest priority) to 2 (highest priority).
|
||||||
|
Default is 2.
|
||||||
|
:param vibration_pattern: If the notification is received on an Android device, the vibration pattern in this
|
||||||
|
field will change the way the device vibrates with it. You can easily create a pattern by going
|
||||||
|
`here <http://autoremotejoaomgcd.appspot.com/AutoRemoteNotification.html>`_ and generating the pattern in
|
||||||
|
the Vibration Pattern field
|
||||||
|
:type vibration_pattern: str (comma-separated float values) or list[float]
|
||||||
|
:param dismiss_on_touch: If set the notification will be dismissed when touched (default: False)
|
||||||
|
:param image: Publicly available URL for an image to show up in the notification
|
||||||
|
:param group: Unique ID to group your notifications with
|
||||||
|
:param sound: Publicly available URL for a sound to play with the notification
|
||||||
|
:param actions: Set notification buttons with customized behaviour. This parameter is a list of Join actions
|
||||||
|
configured on the target device that will be mapped to notification input elements.
|
||||||
|
More info `here <https://joaoapps.com/join/actions/#notifications>`_
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = {'dismissOnTouch': dismiss_on_touch}
|
||||||
|
|
||||||
|
if title:
|
||||||
|
params['title'] = title
|
||||||
|
if text:
|
||||||
|
params['text'] = text
|
||||||
|
if url:
|
||||||
|
params['url'] = url
|
||||||
|
if file:
|
||||||
|
params['file'] = file
|
||||||
|
if icon:
|
||||||
|
params['icon'] = icon
|
||||||
|
if small_icon:
|
||||||
|
params['smallIcon'] = small_icon
|
||||||
|
if priority:
|
||||||
|
params['priority'] = priority
|
||||||
|
if vibration_pattern:
|
||||||
|
params['vibrationPattern'] = [i for i in vibration_pattern.split(',') if len(i)] \
|
||||||
|
if isinstance(vibration_pattern, str) else vibration_pattern
|
||||||
|
assert isinstance(params['vibrationPattern'], list)
|
||||||
|
if image:
|
||||||
|
params['image'] = image
|
||||||
|
if group:
|
||||||
|
params['group'] = group
|
||||||
|
if sound:
|
||||||
|
params['sound'] = sound
|
||||||
|
if actions:
|
||||||
|
actions = [a.strip() for a in actions.split('|||' if '|||' in actions else ',') if len(a.strip())] \
|
||||||
|
if isinstance(actions, str) else actions
|
||||||
|
assert isinstance(actions, list) and len(actions) > 0
|
||||||
|
params['actions'] = '|||'.join(actions)
|
||||||
|
|
||||||
|
return self._send_request(self._push_url, device=device, params=params)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_devices(self):
|
||||||
|
"""
|
||||||
|
:return: List of connected devices, each containing 'id', 'name', 'user' and 'has_tasker' attributes
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'id': dev['deviceId'],
|
||||||
|
'name': dev['deviceName'],
|
||||||
|
'user': dev['userAccount'],
|
||||||
|
'has_tasker': dev['hasTasker'],
|
||||||
|
}
|
||||||
|
for dev in self._send_request(self._devices_url)['records']
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
Loading…
Reference in a new issue