Skip to content

Commit 11bddac

Browse files
committed
Add support for dashboards, blocks and layouts
1 parent d047655 commit 11bddac

File tree

6 files changed

+348
-10
lines changed

6 files changed

+348
-10
lines changed

Adafruit_IO/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@
2121
from .client import Client
2222
from .mqtt_client import MQTTClient
2323
from .errors import AdafruitIOError, RequestError, ThrottlingError, MQTTError
24-
from .model import Data, Feed, Group
25-
from ._version import __version__
24+
from .model import Data, Feed, Group, Dashboard, Block, Layout
25+
from ._version import __version__

Adafruit_IO/client.py

+75-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import requests
2828

2929
from .errors import RequestError, ThrottlingError
30-
from .model import Data, Feed, Group
30+
from .model import Data, Feed, Group, Dashboard, Block, Layout
3131

3232
# set outgoing version, pulled from setup.py
3333
version = pkg_resources.require("Adafruit_IO")[0].version
@@ -278,11 +278,13 @@ def create_feed(self, feed, group_key=None):
278278
:param string feed: Key of Adafruit IO feed.
279279
:param group_key group: Group to place new feed in.
280280
"""
281+
f = feed._asdict()
282+
del f['id'] # Don't pass id on create call
281283
path = "feeds/"
282284
if group_key is not None: # create feed in a group
283285
path="/groups/%s/feeds"%group_key
284-
return Feed.from_dict(self._post(path, {"feed": feed._asdict()}))
285-
return Feed.from_dict(self._post(path, {"feed": feed._asdict()}))
286+
return Feed.from_dict(self._post(path, {"feed": f}))
287+
return Feed.from_dict(self._post(path, {"feed": f}))
286288

287289
def delete_feed(self, feed):
288290
"""Delete the specified feed.
@@ -315,3 +317,73 @@ def delete_group(self, group):
315317
"""
316318
path = "groups/{0}".format(group)
317319
self._delete(path)
320+
321+
# Dashboard functionality.
322+
def dashboards(self, dashboard=None):
323+
"""Retrieve a list of all dashboards, or the specified dashboard.
324+
:param string dashboard: Key of Adafruit IO Dashboard. Defaults to None.
325+
"""
326+
if dashboard is None:
327+
path = "dashboards/"
328+
return list(map(Dashboard.from_dict, self._get(path)))
329+
path = "dashboards/{0}".format(dashboard)
330+
return Dashboard.from_dict(self._get(path))
331+
332+
def create_dashboard(self, dashboard):
333+
"""Create the specified dashboard.
334+
:param Dashboard dashboard: Dashboard object to create
335+
"""
336+
path = "dashboards/"
337+
return Dashboard.from_dict(self._post(path, dashboard._asdict()))
338+
339+
def delete_dashboard(self, dashboard):
340+
"""Delete the specified dashboard.
341+
:param string dashboard: Key of Adafruit IO Dashboard.
342+
"""
343+
path = "dashboards/{0}".format(dashboard)
344+
self._delete(path)
345+
346+
# Block functionality.
347+
def blocks(self, dashboard, block=None):
348+
"""Retrieve a list of all blocks from a dashboard, or the specified block.
349+
:param string dashboard: Key of Adafruit IO Dashboard.
350+
:param string block: id of Adafruit IO Block. Defaults to None.
351+
"""
352+
if block is None:
353+
path = "dashboards/{0}/blocks".format(dashboard)
354+
return list(map(Block.from_dict, self._get(path)))
355+
path = "dashboards/{0}/blocks/{1}".format(dashboard, block)
356+
return Block.from_dict(self._get(path))
357+
358+
def create_block(self, dashboard, block):
359+
"""Create the specified block under the specified dashboard.
360+
:param string dashboard: Key of Adafruit IO Dashboard.
361+
:param Block block: Block object to create under dashboard
362+
"""
363+
path = "dashboards/{0}/blocks".format(dashboard)
364+
return Block.from_dict(self._post(path, block._asdict()))
365+
366+
def delete_block(self, dashboard, block):
367+
"""Delete the specified block.
368+
:param string dashboard: Key of Adafruit IO Dashboard.
369+
:param string block: id of Adafruit IO Block.
370+
"""
371+
path = "dashboards/{0}/blocks/{1}".format(dashboard, block)
372+
self._delete(path)
373+
374+
# Layout functionality.
375+
def layouts(self, dashboard):
376+
"""Retrieve the layouts array from a dashboard
377+
:param string dashboard: key of Adafruit IO Dashboard.
378+
"""
379+
path = "dashboards/{0}".format(dashboard)
380+
dashboard = self._get(path)
381+
return Layout.from_dict(dashboard['layouts'])
382+
383+
def update_layout(self, dashboard, layout):
384+
"""Update the layout of the specified dashboard.
385+
:param string dashboard: Key of Adafruit IO Dashboard.
386+
:param Layout layout: Layout object to update under dashboard
387+
"""
388+
path = "dashboards/{0}/update_layouts".format(dashboard)
389+
return Layout.from_dict(self._post(path, {'layouts': layout._asdict()}))

Adafruit_IO/model.py

+42-2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
FEED_FIELDS = [ 'name',
4545
'key',
46+
'id',
4647
'description',
4748
'unit_type',
4849
'unit_symbol',
@@ -61,6 +62,26 @@
6162
'properties',
6263
'name' ]
6364

65+
DASHBOARD_FIELDS = [ 'name',
66+
'key',
67+
'description',
68+
'show_header',
69+
'color_mode',
70+
'block_borders',
71+
'header_image_url',
72+
'blocks' ]
73+
74+
BLOCK_FIELDS = [ 'name',
75+
'id',
76+
'visual_type',
77+
'properties',
78+
'block_feeds' ]
79+
80+
LAYOUT_FIELDS = ['xl',
81+
'lg',
82+
'md',
83+
'sm',
84+
'xs' ]
6485

6586
# These are very simple data model classes that are based on namedtuple. This is
6687
# to keep the classes simple and prevent any confusion around updating data
@@ -71,15 +92,24 @@
7192
Data = namedtuple('Data', DATA_FIELDS)
7293
Feed = namedtuple('Feed', FEED_FIELDS)
7394
Group = namedtuple('Group', GROUP_FIELDS)
74-
95+
Dashboard = namedtuple('Dashboard', DASHBOARD_FIELDS)
96+
Block = namedtuple('Block', BLOCK_FIELDS)
97+
Layout = namedtuple('Layout', LAYOUT_FIELDS)
7598

7699
# Magic incantation to make all parameters to the initializers optional with a
77100
# default value of None.
78101
Group.__new__.__defaults__ = tuple(None for x in GROUP_FIELDS)
79102
Data.__new__.__defaults__ = tuple(None for x in DATA_FIELDS)
103+
Layout.__new__.__defaults__ = tuple(None for x in LAYOUT_FIELDS)
104+
105+
# explicitly set dashboard values so that 'color_mode' is 'dark'
106+
Dashboard.__new__.__defaults__ = (None, None, None, False, "dark", True, None, None)
107+
108+
# explicitly set block values so 'properties' is a dictionary
109+
Block.__new__.__defaults__ = (None, None, None, {}, None)
80110

81111
# explicitly set feed values
82-
Feed.__new__.__defaults__ = (None, None, None, None, None, 'ON', 'Private', None, None, None)
112+
Feed.__new__.__defaults__ = (None, None, None, None, None, None, 'ON', 'Private', None, None, None)
83113

84114
# Define methods to convert from dicts to the data types.
85115
def _from_dict(cls, data):
@@ -103,7 +133,17 @@ def _group_from_dict(cls, data):
103133
return cls(**params)
104134

105135

136+
def _dashboard_from_dict(cls, data):
137+
params = {x: data.get(x, None) for x in cls._fields}
138+
# Parse the blocks if they're provided and generate block instances.
139+
params['blocks'] = tuple(map(Feed.from_dict, data.get('blocks', [])))
140+
return cls(**params)
141+
142+
106143
# Now add the from_dict class methods defined above to the data types.
107144
Data.from_dict = classmethod(_from_dict)
108145
Feed.from_dict = classmethod(_feed_from_dict)
109146
Group.from_dict = classmethod(_group_from_dict)
147+
Dashboard.from_dict = classmethod(_dashboard_from_dict)
148+
Block.from_dict = classmethod(_from_dict)
149+
Layout.from_dict = classmethod(_from_dict)

examples/basics/dashboard.py

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""
2+
'dashboard.py'
3+
=========================================
4+
Creates a dashboard with 3 blocks and feed it data
5+
6+
Author(s): Doug Zobel
7+
"""
8+
from time import sleep
9+
from random import randrange
10+
from Adafruit_IO import Client, Feed, Block, Dashboard, Layout
11+
12+
# Set to your Adafruit IO key.
13+
# Remember, your key is a secret,
14+
# so make sure not to publish it when you publish this code!
15+
ADAFRUIT_IO_USERNAME = ''
16+
17+
# Set to your Adafruit IO username.
18+
# (go to https://accounts.adafruit.com to find your username)
19+
ADAFRUIT_IO_KEY = ''
20+
21+
# Create an instance of the REST client.
22+
aio = Client(ADAFRUIT_IO_USERNAME, ADAFRUIT_IO_KEY)
23+
24+
# Create a new feed named 'Dashboard Data' under the default group
25+
feed = aio.create_feed(Feed(name="Dashboard Data"), "default")
26+
27+
# Fetch group info (group.id needed when adding feeds to blocks)
28+
group = aio.groups("default")
29+
30+
# Create a new dasbhoard named 'Example Dashboard'
31+
dashboard = aio.create_dashboard(Dashboard(name="Example Dashboard"))
32+
33+
# Create a line_chart
34+
linechart = Block(name="Linechart Data",
35+
visual_type = 'line_chart',
36+
properties = {
37+
"gridLines": True,
38+
"historyHours": "2"},
39+
block_feeds = [{
40+
"group_id": group.id,
41+
"feed_id": feed.id
42+
}])
43+
linechart = aio.create_block(dashboard.key, linechart)
44+
45+
# Create a gauge
46+
gauge = Block(name="Gauge Data",
47+
visual_type = 'gauge',
48+
block_feeds = [{
49+
"group_id": group.id,
50+
"feed_id": feed.id
51+
}])
52+
gauge = aio.create_block(dashboard.key, gauge)
53+
54+
# Create a text stream
55+
stream = Block(name="Stream Data",
56+
visual_type = 'stream',
57+
properties = {
58+
"fontSize": "12",
59+
"fontColor": "#63de00",
60+
"showGroupName": "no"},
61+
block_feeds = [{
62+
"group_id": group.id,
63+
"feed_id": feed.id
64+
}])
65+
stream = aio.create_block(dashboard.key, stream)
66+
67+
# Update the large layout to:
68+
# |----------------|
69+
# | Line Chart |
70+
# |----------------|
71+
# | Gauge | Stream |
72+
# |----------------|
73+
layout = Layout(lg = [
74+
{'x': 0, 'y': 0, 'w': 16, 'h': 4, 'i': str(linechart.id)},
75+
{'x': 0, 'y': 4, 'w': 8, 'h': 4, 'i': str(gauge.id)},
76+
{'x': 8, 'y': 4, 'w': 8, 'h': 4, 'i': str(stream.id)}])
77+
aio.update_layout(dashboard.key, layout)
78+
79+
print("Dashboard created at: " +
80+
"https://io.adafruit.com/{0}/dashboards/{1}".format(ADAFRUIT_IO_USERNAME,
81+
dashboard.key))
82+
# Now send some data
83+
value = 0
84+
while True:
85+
value = (value + randrange(0, 10)) % 100
86+
print('sending data: ', value)
87+
aio.send_data(feed.key, value)
88+
sleep(3)

tests/test_client.py

+104-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import time
44
import unittest
55

6-
from Adafruit_IO import Client, Data, Feed, Group, RequestError
6+
from Adafruit_IO import Client, Data, Feed, Group, Dashboard, Block, Layout, RequestError
77

88
import base
99

@@ -46,6 +46,22 @@ def ensure_group_deleted(self, client, group):
4646
# Swallow the error if the group doesn't exist.
4747
pass
4848

49+
def ensure_dashboard_deleted(self, client, dashboard):
50+
# Delete the specified dashboard if it exists.
51+
try:
52+
client.delete_dashboard(dashboard)
53+
except RequestError:
54+
# Swallow the error if the dashboard doesn't exist.
55+
pass
56+
57+
def ensure_block_deleted(self, client, dashboard, block):
58+
# Delete the specified block if it exists.
59+
try:
60+
client.delete_block(dashboard, block)
61+
except RequestError:
62+
# Swallow the error if the block doesn't exist.
63+
pass
64+
4965
def empty_feed(self, client, feed):
5066
# Remove all the data from a specified feed (but don't delete the feed).
5167
data = client.data(feed)
@@ -269,3 +285,90 @@ def test_receive_group_by_key(self):
269285
group = io.create_group(Group(name='grouprx'))
270286
response = io.groups(group.key)
271287
self.assertEqual(response.key, 'grouprx')
288+
289+
# Test Dashboard Functionality
290+
def test_dashboard_create_dashboard(self):
291+
io = self.get_client()
292+
self.ensure_dashboard_deleted(io, 'dashtest')
293+
response = io.create_dashboard(Dashboard(name='dashtest'))
294+
self.assertEqual(response.name, 'dashtest')
295+
296+
def test_dashboard_returns_all_dashboards(self):
297+
io = self.get_client()
298+
self.ensure_dashboard_deleted(io, 'dashtest')
299+
dashboard = io.create_dashboard(Dashboard(name='dashtest'))
300+
response = io.dashboards()
301+
self.assertGreaterEqual(len(response), 1)
302+
303+
def test_dashboard_returns_requested_feed(self):
304+
io = self.get_client()
305+
self.ensure_dashboard_deleted(io, 'dashtest')
306+
dashboard = io.create_dashboard(Dashboard(name='dashtest'))
307+
response = io.dashboards('dashtest')
308+
self.assertEqual(response.name, 'dashtest')
309+
310+
# Test Block Functionality
311+
def test_block_create_block(self):
312+
io = self.get_client()
313+
self.ensure_block_deleted(io, 'dashtest', 'blocktest')
314+
self.ensure_dashboard_deleted(io, 'dashtest')
315+
dash = io.create_dashboard(Dashboard(name='dashtest'))
316+
block = io.create_block(dash.key, Block(name='blocktest',
317+
visual_type = 'line_chart'))
318+
self.assertEqual(block.name, 'blocktest')
319+
io.delete_block(dash.key, block.id)
320+
io.delete_dashboard(dash.key)
321+
322+
def test_dashboard_returns_all_blocks(self):
323+
io = self.get_client()
324+
self.ensure_block_deleted(io, 'dashtest', 'blocktest')
325+
self.ensure_dashboard_deleted(io, 'dashtest')
326+
dash = io.create_dashboard(Dashboard(name='dashtest'))
327+
block = io.create_block(dash.key, Block(name='blocktest',
328+
visual_type = 'line_chart'))
329+
response = io.blocks(dash.key)
330+
self.assertEqual(len(response), 1)
331+
io.delete_block(dash.key, block.id)
332+
io.delete_dashboard(dash.key)
333+
334+
def test_dashboard_returns_requested_block(self):
335+
io = self.get_client()
336+
self.ensure_block_deleted(io, 'dashtest', 'blocktest')
337+
self.ensure_dashboard_deleted(io, 'dashtest')
338+
dash = io.create_dashboard(Dashboard(name='dashtest'))
339+
block = io.create_block(dash.key, Block(name='blocktest',
340+
visual_type = 'line_chart'))
341+
response = io.blocks(dash.key, block.id)
342+
self.assertEqual(response.name, 'blocktest')
343+
io.delete_block(dash.key, block.id)
344+
io.delete_dashboard(dash.key)
345+
346+
# Test Layout Functionality
347+
def test_layout_returns_all_layouts(self):
348+
io = self.get_client()
349+
self.ensure_block_deleted(io, 'dashtest', 'blocktest')
350+
self.ensure_dashboard_deleted(io, 'dashtest')
351+
dash = io.create_dashboard(Dashboard(name='dashtest'))
352+
block = io.create_block(dash.key, Block(name='blocktest',
353+
visual_type = 'line_chart'))
354+
response = io.layouts(dash.key)
355+
self.assertEqual(len(response), 5) # 5 layouts: xs, sm, md, lg, xl
356+
self.assertEqual(len(response.lg), 1)
357+
io.delete_block(dash.key, block.id)
358+
io.delete_dashboard(dash.key)
359+
360+
def test_layout_update_layout(self):
361+
io = self.get_client()
362+
self.ensure_block_deleted(io, 'dashtest', 'blocktest')
363+
self.ensure_dashboard_deleted(io, 'dashtest')
364+
dash = io.create_dashboard(Dashboard(name='dashtest'))
365+
block = io.create_block(dash.key, Block(name='blocktest',
366+
visual_type = 'line_chart'))
367+
layout = Layout(lg = [
368+
{'x': 0, 'y': 0, 'w': 16, 'h': 4, 'i': str(block.id)}])
369+
io.update_layout(dash.key, layout)
370+
response = io.layouts(dash.key)
371+
self.assertEqual(len(response.lg), 1)
372+
self.assertEqual(response.lg[0]['w'], 16)
373+
io.delete_block(dash.key, block.id)
374+
io.delete_dashboard(dash.key)

0 commit comments

Comments
 (0)