Skip to content

Commit c152480

Browse files
committed
Merge branch 'master' into get-pages
# Conflicts: # tests/test_client.py
2 parents 3d267e9 + 92093d4 commit c152480

File tree

8 files changed

+412
-150
lines changed

8 files changed

+412
-150
lines changed

.github/workflows/build.yml

+11
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,20 @@ name: Build-CI
33
on: [pull_request, push]
44

55
jobs:
6+
7+
approve: # First step
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- name: Approve
12+
run: echo For security reasons, all pull requests to this repository need to be approved first before running any automated CI.
13+
614
build:
715
runs-on: ubuntu-latest
816

17+
needs: [approve] # Require the first step to finish
18+
environment:
19+
name: IO
920
steps:
1021
- uses: actions/checkout@v2
1122

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

+89-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1919
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2020
# SOFTWARE.
21+
import time
2122
from time import struct_time
2223
import json
2324
import platform
@@ -30,7 +31,7 @@
3031
import requests
3132

3233
from .errors import RequestError, ThrottlingError
33-
from .model import Data, Feed, Group
34+
from .model import Data, Feed, Group, Dashboard, Block, Layout
3435

3536
API_PAGE_LIMIT = 1000
3637

@@ -194,9 +195,19 @@ def receive_time(self):
194195
https://docs.python.org/3.7/library/time.html#time.struct_time
195196
"""
196197
path = 'integrations/time/struct.json'
197-
time = self._get(path)
198-
return struct_time((time['year'], time['mon'], time['mday'], time['hour'],
199-
time['min'], time['sec'], time['wday'], time['yday'], time['isdst']))
198+
return self._parse_time_struct(self._get(path))
199+
200+
@staticmethod
201+
def _parse_time_struct(time_dict: dict) -> time.struct_time:
202+
"""Parse the time data returned by the server and return a time_struct
203+
204+
Corrects for the weekday returned by the server in Sunday=0 format
205+
(Python expects Monday=0)
206+
"""
207+
wday = (time_dict['wday'] - 1) % 7
208+
return struct_time((time_dict['year'], time_dict['mon'], time_dict['mday'],
209+
time_dict['hour'], time_dict['min'], time_dict['sec'],
210+
wday, time_dict['yday'], time_dict['isdst']))
200211

201212
def receive_weather(self, weather_id=None):
202213
"""Adafruit IO Weather Service, Powered by Dark Sky
@@ -326,11 +337,13 @@ def create_feed(self, feed, group_key=None):
326337
:param string feed: Key of Adafruit IO feed.
327338
:param group_key group: Group to place new feed in.
328339
"""
340+
f = feed._asdict()
341+
del f['id'] # Don't pass id on create call
329342
path = "feeds/"
330343
if group_key is not None: # create feed in a group
331344
path="/groups/%s/feeds"%group_key
332-
return Feed.from_dict(self._post(path, {"feed": feed._asdict()}))
333-
return Feed.from_dict(self._post(path, {"feed": feed._asdict()}))
345+
return Feed.from_dict(self._post(path, {"feed": f}))
346+
return Feed.from_dict(self._post(path, {"feed": f}))
334347

335348
def delete_feed(self, feed):
336349
"""Delete the specified feed.
@@ -363,3 +376,73 @@ def delete_group(self, group):
363376
"""
364377
path = "groups/{0}".format(group)
365378
self._delete(path)
379+
380+
# Dashboard functionality.
381+
def dashboards(self, dashboard=None):
382+
"""Retrieve a list of all dashboards, or the specified dashboard.
383+
:param string dashboard: Key of Adafruit IO Dashboard. Defaults to None.
384+
"""
385+
if dashboard is None:
386+
path = "dashboards/"
387+
return list(map(Dashboard.from_dict, self._get(path)))
388+
path = "dashboards/{0}".format(dashboard)
389+
return Dashboard.from_dict(self._get(path))
390+
391+
def create_dashboard(self, dashboard):
392+
"""Create the specified dashboard.
393+
:param Dashboard dashboard: Dashboard object to create
394+
"""
395+
path = "dashboards/"
396+
return Dashboard.from_dict(self._post(path, dashboard._asdict()))
397+
398+
def delete_dashboard(self, dashboard):
399+
"""Delete the specified dashboard.
400+
:param string dashboard: Key of Adafruit IO Dashboard.
401+
"""
402+
path = "dashboards/{0}".format(dashboard)
403+
self._delete(path)
404+
405+
# Block functionality.
406+
def blocks(self, dashboard, block=None):
407+
"""Retrieve a list of all blocks from a dashboard, or the specified block.
408+
:param string dashboard: Key of Adafruit IO Dashboard.
409+
:param string block: id of Adafruit IO Block. Defaults to None.
410+
"""
411+
if block is None:
412+
path = "dashboards/{0}/blocks".format(dashboard)
413+
return list(map(Block.from_dict, self._get(path)))
414+
path = "dashboards/{0}/blocks/{1}".format(dashboard, block)
415+
return Block.from_dict(self._get(path))
416+
417+
def create_block(self, dashboard, block):
418+
"""Create the specified block under the specified dashboard.
419+
:param string dashboard: Key of Adafruit IO Dashboard.
420+
:param Block block: Block object to create under dashboard
421+
"""
422+
path = "dashboards/{0}/blocks".format(dashboard)
423+
return Block.from_dict(self._post(path, block._asdict()))
424+
425+
def delete_block(self, dashboard, block):
426+
"""Delete the specified block.
427+
:param string dashboard: Key of Adafruit IO Dashboard.
428+
:param string block: id of Adafruit IO Block.
429+
"""
430+
path = "dashboards/{0}/blocks/{1}".format(dashboard, block)
431+
self._delete(path)
432+
433+
# Layout functionality.
434+
def layouts(self, dashboard):
435+
"""Retrieve the layouts array from a dashboard
436+
:param string dashboard: key of Adafruit IO Dashboard.
437+
"""
438+
path = "dashboards/{0}".format(dashboard)
439+
dashboard = self._get(path)
440+
return Layout.from_dict(dashboard['layouts'])
441+
442+
def update_layout(self, dashboard, layout):
443+
"""Update the layout of the specified dashboard.
444+
:param string dashboard: Key of Adafruit IO Dashboard.
445+
:param Layout layout: Layout object to update under dashboard
446+
"""
447+
path = "dashboards/{0}/update_layouts".format(dashboard)
448+
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(Block.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)

0 commit comments

Comments
 (0)