Completed documentation for plugins

This commit is contained in:
Fabio Manganiello 2018-06-25 19:57:43 +02:00
parent 1cbef67f2c
commit b876f17f81
23 changed files with 661 additions and 16 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
``platypush.plugins.video.omxplayer``
=====================================
.. automodule:: platypush.plugins.video.omxplayer
:members:

View file

@ -0,0 +1,6 @@
``platypush.plugins.weather.forecast``
======================================
.. automodule:: platypush.plugins.weather.forecast
:members:

View file

@ -26,4 +26,15 @@ Plugins
platypush/plugins/midi.rst
platypush/plugins/mqtt.rst
platypush/plugins/music.mpd.rst
platypush/plugins/pushbullet.rst
platypush/plugins/redis.rst
platypush/plugins/serial.rst
platypush/plugins/shell.rst
platypush/plugins/switch.rst
platypush/plugins/switch.switchbot.rst
platypush/plugins/switch.wemo.rst
platypush/plugins/tts.rst
platypush/plugins/variable.rst
platypush/plugins/video.omxplayer.rst
platypush/plugins/weather.forecast.rst

View file

@ -8,7 +8,24 @@ from platypush.plugins import Plugin
class PushbulletPlugin(Plugin):
"""
This plugin allows you to send pushes and files to your PushBullet account.
Note: This plugin will only work if the :mod:`platypush.backend.pushbullet`
backend is configured.
Requires:
* **requests** (``pip install requests``)
"""
def send_push(self, **kwargs):
"""
Send a push.
:param kwargs: Push arguments, see https://docs.pushbullet.com/#create-push
:type kwargs: dict
"""
pushbullet = get_backend('pushbullet')
resp = requests.post('https://api.pushbullet.com/v2/ephemerals',
data=json.dumps({
@ -27,6 +44,13 @@ class PushbulletPlugin(Plugin):
def send_file(self, filename):
"""
Send a file.
:param filename: Path to the local file
:type filename: str
"""
pushbullet = get_backend('pushbullet')
resp = requests.post('https://api.pushbullet.com/v2/upload-request',
data=json.dumps({'file_name': os.path.basename(filename)}),

View file

@ -5,7 +5,31 @@ from platypush.plugins import Plugin
class RedisPlugin(Plugin):
"""
Plugin to send messages on Redis queues.
Requires:
* **redis** (``pip install redis``)
"""
def send_message(self, queue, msg, *args, **kwargs):
"""
Send a message to a Redis queu.
:param queue: Queue name
:type queue: str
:param msg: Message to be sent
:type msg: str, bytes, list, dict, Message object
:param args: Args passed to the Redis constructor (see https://redis-py.readthedocs.io/en/latest/#redis.Redis)
:type args: list
:param kwargs: Kwargs passed to the Redis constructor (see https://redis-py.readthedocs.io/en/latest/#redis.Redis)
:type kwargs: dict
"""
redis = Redis(*args, **kwargs)
redis.rpush(queue, msg)
return Response(output={'state': 'ok'})

View file

@ -17,6 +17,14 @@ class SerialPlugin(Plugin):
"""
def __init__(self, device, baud_rate=9600, *args, **kwargs):
"""
:param device: Device path (e.g. ``/dev/ttyUSB0`` or ``/dev/ttyACM0``)
:type device: str
:param baud_rate: Serial baud rate (default: 9600)
:type baud_rate: int
"""
super().__init__(*args, **kwargs)
self.device = device
@ -53,6 +61,10 @@ class SerialPlugin(Plugin):
return output.decode().strip()
def get_data(self):
"""
Reads JSON data from the serial device and returns it as a message
"""
ser = serial.Serial(self.device, self.baud_rate)
try:

View file

@ -5,7 +5,20 @@ from platypush.message.response import Response
from .. import Plugin
class ShellPlugin(Plugin):
"""
Plugin to run custom shell commands.
"""
def exec(self, cmd):
"""
Execute a command.
:param cmd: Command to execute
:type cmd: str
:returns: A response object where the ``output`` field will contain the command output as a string, and the ``errors`` field will contain whatever was sent to stderr.
"""
output = None
errors = []

View file

@ -1,16 +1,24 @@
from .. import Plugin
class SwitchPlugin(Plugin):
"""
Abstract class for interacting with switch devices
"""
def on(self, args):
""" Turn the device on """
raise NotImplementedError()
def off(self, args):
""" Turn the device off """
raise NotImplementedError()
def toggle(self, args):
""" Toggle the device status (on/off) """
raise NotImplementedError()
def status(self):
""" Get the device state """
raise NotImplementedError()

View file

@ -80,15 +80,38 @@ class Driver(object):
class SwitchSwitchbotPlugin(SwitchPlugin):
"""
Plugin to interact with a Switchbot (https://www.switch-bot.com/) device and
programmatically control buttons.
NOTE: since the interaction with the Switchbot requires root privileges
(in order to scan on the bluetooth interface or setting gattlib in random),
this plugin just wraps the module into a `sudo` flavor, since running
Platypush with root privileges should be considered as a very bad idea.
Make sure that your user has sudo privileges for running this bit of code.
Make sure that your user has sudo privileges for running this plugin.
Requires:
* **pybluez** (``pip install pybluez``)
* **gattlib** (``pip install gattlib``)
* **libboost** (on Debian ```apt-get install libboost-python-dev libboost-thread-dev``)
"""
def __init__(self, bt_interface=None, connect_timeout=None,
scan_timeout=None, devices={}, *args, **kwargs):
"""
:param bt_interface: Bluetooth interface to use (e.g. hci0) default: first available one
:type bt_interface: str
:param connecct_timeout: Timeout for the conncection to the Switchbot device - default: None
:type connect_timeout: float
:param scan_timeout: Timeout for the scan operations - default: None
:type scan_timeout: float
:param devices: Devices to control, as a BMAC address -> name map
:type devices: dict
"""
self.bt_interface = bt_interface
self.connect_timeout = connect_timeout if connect_timeout else 5
self.scan_timeout = scan_timeout if scan_timeout else 2
@ -118,15 +141,34 @@ class SwitchSwitchbotPlugin(SwitchPlugin):
def press(self, device):
"""
Send a press button command to a device
:param device: Device name or address
:type device: str
"""
return self._run(device)
def on(self, device):
"""
Send a press-on button command to a device
:param device: Device name or address
:type device: str
"""
return self._run(device, 'on')
def off(self, device):
"""
Send a press-off button command to a device
:param device: Device name or address
:type device: str
"""
return self._run(device, 'off')
def scan(self):
""" Scan for available Switchbot devices nearby """
output = None
errors = []
@ -145,5 +187,6 @@ class SwitchSwitchbotPlugin(SwitchPlugin):
return Response(output=output, errors=errors)
# vim:sw=4:ts=4:et:

View file

@ -6,7 +6,20 @@ from platypush.message.response import Response
from .. import SwitchPlugin
class SwitchWemoPlugin(SwitchPlugin):
"""
Plugin to control a Belkin WeMo smart switch
(https://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)
Requires:
* **ouimeaux** (``pip install ouimeaux``)
"""
def __init__(self, discovery_seconds=3, *args, **kwargs):
"""
:param discovery_seconds: Discovery time when scanning for devices (default: 3)
:type discovery_seconds: int
"""
super().__init__(*args, **kwargs)
self.discovery_seconds=discovery_seconds
@ -15,11 +28,34 @@ class SwitchWemoPlugin(SwitchPlugin):
self.refresh_devices()
def refresh_devices(self):
""" Update the list of available devices """
self.logger.info('Starting WeMo discovery')
self.env.discover(seconds=self.discovery_seconds)
self.devices = self.env.devices
def get_devices(self):
"""
Get the list of available devices
:returns: The list of devices.
Example output::
output = {
"devices": [
{
"host": "192.168.1.123",
"name": "Switch 1",
"state": 1,
"model": "Belkin Plugin Socket 1.0",
"serialnumber": "123456ABCDEF"
},
{
# ...
}
]
}
"""
self.refresh_devices()
return Response(
output = { 'devices': [
@ -49,12 +85,30 @@ class SwitchWemoPlugin(SwitchPlugin):
return Response(output=json.dumps(resp))
def on(self, device):
"""
Turn a switch on
:param device: Device name
:type device: str
"""
return self._exec('on', device)
def off(self, device):
"""
Turn a switch off
:param device: Device name
:type device: str
"""
return self._exec('off', device)
def toggle(self, device):
"""
Toggle the state of a switch (on/off)
:param device: Device name
:type device: str
"""
return self._exec('toggle', device)

View file

@ -6,14 +6,28 @@ from platypush.message.response import Response
from .. import Plugin
class TtsPlugin(Plugin):
""" Default Text-to-Speech plugin. It leverages Google Translate and
requires mplayer """
"""
Default Text-to-Speech plugin. It leverages Google Translate.
Requires:
* **mplayer** - see your distribution docs on how to install the mplayer package
"""
def __init__(self, lang='en-gb'):
super().__init__()
self.lang=lang
def say(self, phrase, lang=None):
"""
Say a phrase
:param phrase: Phrase to say
:type phrase: str
:param lang: Language code
:type lang: str
"""
if lang is None: lang=self.lang
output = None
errors = []
@ -34,5 +48,6 @@ class TtsPlugin(Plugin):
return Response(output=output, errors=errors)
# vim:sw=4:ts=4:et:

View file

@ -13,14 +13,37 @@ class VariablePlugin(Plugin):
self._variables = {}
def get(self, name, default_value=None):
"""
Get the value of a variable by name.
:param name: Variable name
:type name: str
:param default_value: What will be returned if the variable is not defined (default: None)
:returns: A map in the format ``{"<name>":"<value>"}``
"""
return Response(output={name: self._variables.get(name, default_value)})
def set(self, **kwargs):
"""
Set a variable or a set of variables.
:param kwargs: Key-value list of variables to set (e.g. ``foo='bar', answer=42``)
"""
for (name, value) in kwargs.items():
self._variables[name] = value
return Response(output=kwargs)
def unset(self, name):
"""
Unset a variable by name if it's set
:param name: Name of the variable to remove
:type name: str
"""
if name in self._variables:
del self._variables[name]
return Response(output={'status':'ok'})

View file

@ -19,6 +19,22 @@ from platypush.message.event.video import VideoPlayEvent, VideoPauseEvent, \
from .. import Plugin
class VideoOmxplayerPlugin(Plugin):
"""
Plugin to control video and media playback on your Raspberry Pi or
ARM-compatible device using OMXPlayer.
It can play local files, remote URLs, YouTube URLs and it supports torrents
search, download and play.
Requires:
* **omxplayer** installed on your system (see your distro instructions)
* **omxplayer-wrapper** (``pip install omxplayer-wrapper``)
* **python-libtorrent** (``pip install python-libtorrent``), optional for Torrent support
* **youtube-dl** installed on your system (see your distro instructions), optional for YouTube support
"""
""" Supported video extensions """
video_extensions = {
'.avi', '.flv', '.wmv', '.mov', '.mp4', '.m4v', '.mpg', '.mpeg',
'.rm', '.swf', '.vob', '.mkv'
@ -28,6 +44,20 @@ class VideoOmxplayerPlugin(Plugin):
torrent_state = {}
def __init__(self, args=[], media_dirs=[], download_dir=None, torrent_ports=[], *argv, **kwargs):
"""
:param args: Arguments that will be passed to the OMXPlayer constructor (e.g. subtitles, volume, start position, window size etc.) see https://github.com/popcornmix/omxplayer#synopsis and http://python-omxplayer-wrapper.readthedocs.io/en/latest/omxplayer/#omxplayer.player.OMXPlayer
:type args: list
:param media_dirs: Directories that will be scanned for media files when a search is performed (default: none)
:type media_dirs: list
:param download_dir: Directory where the videos/torrents will be downloaded (default: none)
:type download_dir: str
:param torrent_ports: Torrent ports to listen on (default: 6881 and 6891)
:type torrent_ports: list[int]
"""
super().__init__(*argv, **kwargs)
self.args = args
@ -54,6 +84,17 @@ class VideoOmxplayerPlugin(Plugin):
self.torrent_ports = torrent_ports if torrent_ports else self.default_torrent_ports
def play(self, resource):
"""
Play a resource.
:param resource: Resource to play. Supported types:
* Local files (format: ``file://<path>/<file>``)
* Remote videos (format: ``https://<url>/<resource>``)
* YouTube videos (format: ``https://www.youtube.com/watch?v=<id>``)
* Torrents (format: ``magnet:?<magnet_uri>``)
"""
if resource.startswith('youtube:') \
or resource.startswith('https://www.youtube.com/watch?v='):
resource = self._get_youtube_content(resource)
@ -90,9 +131,11 @@ class VideoOmxplayerPlugin(Plugin):
return self.status()
def pause(self):
""" Pause the playback """
if self.player: self.player.play_pause()
def stop(self):
""" Stop the playback """
if self.player:
self.player.stop()
self.player.quit()
@ -101,26 +144,31 @@ class VideoOmxplayerPlugin(Plugin):
return self.status()
def voldown(self):
""" Volume down by 10% """
if self.player:
self.player.set_volume(max(-6000, self.player.volume()-1000))
return self.status()
def volup(self):
""" Volume up by 10% """
if self.player:
self.player.set_volume(min(0, self.player.volume()+1000))
return self.status()
def back(self):
""" Back by 30 seconds """
if self.player:
self.player.seek(-30)
return self.status()
def forward(self):
""" Forward by 30 seconds """
if self.player:
self.player.seek(+30)
return self.status()
def next(self):
""" Play the next track/video """
if self.player:
self.player.stop()
@ -132,48 +180,103 @@ class VideoOmxplayerPlugin(Plugin):
def hide_subtitles(self):
""" Hide the subtitles """
if self.player: self.player.hide_subtitles()
return self.status()
def hide_video(self):
""" Hide the video """
if self.player: self.player.hide_video()
return self.status()
def is_playing(self):
"""
:returns: True if it's playing, False otherwise
"""
if self.player: return self.player.is_playing()
else: return False
def load(self, source, pause=False):
if self.player: self.player.load(source, pause)
def load(self, resource, pause=False):
"""
Load a resource/video in the player.
:param pause: If set, load the video in paused mode (default: False)
:type pause: bool
"""
if self.player: self.player.load(resource, pause)
return self.status()
def metadata(self):
""" Get the metadata of the current video """
if self.player: return Response(output=self.player.metadata())
return self.status()
def mute(self):
""" Mute the player """
if self.player: self.player.mute()
return self.status()
def unmute(self):
""" Unmute the player """
if self.player: self.player.unmute()
return self.status()
def seek(self, relative_position):
"""
Seek backward/forward by the specified number of seconds
:param relative_position: Number of seconds relative to the current cursor
:type relative_position: int
"""
if self.player: self.player.seek(relative_position)
return self.status()
def set_position(self, position):
"""
Seek backward/forward to the specified absolute position
:param position: Number of seconds from the start
:type position: int
"""
if self.player: self.player.set_seek(position)
return self.status()
def set_volume(self, volume):
"""
Set the volume
:param volume: Volume value between 0 and 100
:type volume: int
"""
# Transform a [0,100] value to an OMXPlayer volume in [-6000,0]
volume = 60.0*volume - 6000
if self.player: self.player.set_volume(volume)
return self.status()
def status(self):
"""
Get the current player state.
:returns: A dictionary containing the current state.
Example::
output = {
"source": "https://www.youtube.com/watch?v=7L9KkZoNZkA",
"state": "play",
"volume": 80,
"elapsed": 123,
"duration": 300,
"width": 800,
"height": 600
}
"""
state = PlayerState.STOP.value
if self.player:
@ -233,6 +336,22 @@ class VideoOmxplayerPlugin(Plugin):
self.player.stopEvent += self.on_stop()
def search(self, query, types=None, queue_results=False, autoplay=False):
"""
Perform a video search.
:param query: Query string, video name or partial name
:type query: str
:param types: Video types to search (default: ``["youtube", "file", "torrent"]``)
:type types: list
:param queue_results: Append the results to the current playing queue (default: False)
:type queue_results: bool
:param autoplay: Play the first result of the search (default: False)
:type autoplay: bool
"""
results = []
if types is None:
types = { 'youtube', 'file', 'torrent' }
@ -360,6 +479,13 @@ class VideoOmxplayerPlugin(Plugin):
return Response(output=results)
def download_torrent(self, magnet):
"""
Download a torrent to ``download_dir`` by Magnet URI
:param magnet: Magnet URI
:type magnet: str
"""
import libtorrent as lt
if not self.download_dir:

View file

@ -3,27 +3,253 @@ from platypush.plugins.http.request import HttpRequestPlugin
class WeatherForecastPlugin(HttpRequestPlugin):
""" Plugin for getting weather updates through Darksky API """
"""
Plugin for getting weather updates through Darksky API
Requires:
* **requests** (``pip install requests``)
"""
def __init__(self, darksky_token, lat, long, units='si', **kwargs):
""" Supported unit types: ca, uk2, us, si """
"""
:param darksky_token: Your token for using the darksky API, see https://darksky.net/dev
:type darksky_token: str
:param lat: Default forecast latitude
:type lat: float
:param long: Default forecast longitude
:type long: float
:param units: Weather units (default: "si").
Supported units:
* **si** (international system)
* **us** (US imperial units)
* **uk** (UK imperial units)
* **ca** (Canada imperial units)
:type units: str
"""
super().__init__(method='get', output='json')
self.darksky_token = darksky_token
self.units = units
self.lat = lat
self.long = long
self.latest_bulletin = {}
self.url = 'https://api.darksky.net/forecast/{}/{},{}?units={}'. \
format(darksky_token, lat, long, units)
def get_current_weather(self, **kwargs):
response = self.get(self.url)
print(response)
def _get_url(self, lat=None, long=None):
return 'https://api.darksky.net/forecast/{}/{},{}?units={}'. \
format(self.darksky_token, (lat or self.lat), (long or self.long),
self.units)
def get_current_weather(self, lat=None, long=None, **kwargs):
"""
Get the current weather.
:param lat: Weather latitude (default: configured latitude)
:type lat: float
:param long: Weather longitude (default: configured longitude)
:type long: float
:returns: A dictionary containing the current weather object.
Example output::
output = {
"time": 1529947892,
"summary": "Mostly Cloudy",
"icon": "partly-cloudy-day",
"precipIntensity": 0.0483,
"precipProbability": 0.04,
"precipType": "rain",
"temperature": 27.94,
"apparentTemperature": 29.6,
"dewPoint": 20.01,
"humidity": 0.62,
"pressure": 1009.34,
"windSpeed": 1.83,
"windGust": 5.49,
"windBearing": 192,
"cloudCover": 0.66,
"uvIndex": 0,
"visibility": 16.09,
"ozone": 273.74
}
"""
response = self.get(self._get_url(lat, long))
return Response(output=response.output['currently'])
def get_hourly_forecast(self, **kwargs):
response = self.get(self.url)
def get_hourly_forecast(self, lat=None, long=None, **kwargs):
"""
Get the hourly forecast.
:param lat: Weather latitude (default: configured latitude)
:type lat: float
:param long: Weather longitude (default: configured longitude)
:type long: float
:returns: A forecast object.
Example output::
output = {
"summary": "Partly cloudy starting tomorrow morning, continuing until tomorrow evening.",
"icon": "partly-cloudy-day",
"data": [
{
"time": 1529946000,
"summary": "Clear",
"icon": "clear-day",
"precipIntensity": 0,
"precipProbability": 0,
"temperature": 18.94,
"apparentTemperature": 18.94,
"dewPoint": 11.99,
"humidity": 0.64,
"pressure": 1025.53,
"windSpeed": 5.1,
"windGust": 6.22,
"windBearing": 329,
"cloudCover": 0.14,
"uvIndex": 1,
"visibility": 14.19,
"ozone": 334.3
},
{
"time": 1529949600,
"summary": "Clear",
"icon": "clear-day",
"precipIntensity": 0,
"precipProbability": 0,
"temperature": 18.41,
"apparentTemperature": 18.41,
"dewPoint": 11.12,
"humidity": 0.63,
"pressure": 1025.54,
"windSpeed": 4.6,
"windGust": 6.18,
"windBearing": 340,
"cloudCover": 0.07,
"uvIndex": 1,
"visibility": 16.09,
"ozone": 333.53
},
# ...
}
"""
response = self.get(self._get_url(lat, long))
return Response(output=response.output['hourly'])
def get_daily_forecast(self, **kwargs):
response = self.get(self.url)
def get_daily_forecast(self, lat=None, long=None, **kwargs):
"""
Get the daily forecast.
:param lat: Weather latitude (default: configured latitude)
:type lat: float
:param long: Weather longitude (default: configured longitude)
:type long: float
:returns: A forecast object.
Example output::
"output": {
"summary": "Light rain on Sunday, with high temperatures rising to 28°C on Sunday.",
"icon": "rain",
"data": [
{
"time": 1529877600,
"summary": "Mostly cloudy until afternoon.",
"icon": "partly-cloudy-day",
"sunriseTime": 1529896835,
"sunsetTime": 1529957280,
"moonPhase": 0.42,
"precipIntensity": 0,
"precipIntensityMax": 0.0051,
"precipIntensityMaxTime": 1529888400,
"precipProbability": 0,
"temperatureHigh": 20.04,
"temperatureHighTime": 1529931600,
"temperatureLow": 10.68,
"temperatureLowTime": 1529982000,
"apparentTemperatureHigh": 20.04,
"apparentTemperatureHighTime": 1529931600,
"apparentTemperatureLow": 10.68,
"apparentTemperatureLowTime": 1529982000,
"dewPoint": 12.18,
"humidity": 0.77,
"pressure": 1025.16,
"windSpeed": 3.84,
"windGust": 6.51,
"windGustTime": 1529881200,
"windBearing": 336,
"cloudCover": 0.5,
"uvIndex": 6,
"uvIndexTime": 1529928000,
"visibility": 14.08,
"ozone": 331.24,
"temperatureMin": 13.89,
"temperatureMinTime": 1529960400,
"temperatureMax": 20.04,
"temperatureMaxTime": 1529931600,
"apparentTemperatureMin": 13.89,
"apparentTemperatureMinTime": 1529960400,
"apparentTemperatureMax": 20.04,
"apparentTemperatureMaxTime": 1529931600
},
{
"time": 1529964000,
"summary": "Partly cloudy throughout the day.",
"icon": "partly-cloudy-day",
"sunriseTime": 1529983261,
"sunsetTime": 1530043677,
"moonPhase": 0.45,
"precipIntensity": 0,
"precipIntensityMax": 0,
"precipProbability": 0,
"temperatureHigh": 20.95,
"temperatureHighTime": 1530018000,
"temperatureLow": 11.47,
"temperatureLowTime": 1530064800,
"apparentTemperatureHigh": 20.95,
"apparentTemperatureHighTime": 1530018000,
"apparentTemperatureLow": 11.47,
"apparentTemperatureLowTime": 1530064800,
"dewPoint": 10.19,
"humidity": 0.69,
"pressure": 1026.14,
"windSpeed": 3.67,
"windGust": 7.13,
"windGustTime": 1530036000,
"windBearing": 4,
"cloudCover": 0.3,
"uvIndex": 5,
"uvIndexTime": 1530010800,
"visibility": 16.09,
"ozone": 328.59,
"temperatureMin": 10.68,
"temperatureMinTime": 1529982000,
"temperatureMax": 20.95,
"temperatureMaxTime": 1530018000,
"apparentTemperatureMin": 10.68,
"apparentTemperatureMinTime": 1529982000,
"apparentTemperatureMax": 20.95,
"apparentTemperatureMaxTime": 1530018000
},
# ...
}
"""
response = self.get(self._get_url(lat, long))
return Response(output=response.output['daily'])