Conversation
There was a problem hiding this comment.
Pull request overview
This PR rewrites the examples/basics/dashboard.py example to demonstrate creating a dashboard with every supported Adafruit IO block type via the REST API (previously only 3 block types were shown). It also makes minor documentation improvements to two weather example files.
Changes:
examples/basics/dashboard.py: Complete rewrite — feeds are bootstrapped, account limits are checked, one block of each visual type is created, and a responsive layout is applied.examples/api/weather.py: Docstring updated from "Dark Sky Hyperlocal" to "Apple WeatherKit example" and an inline comment added to thereceive_weathercall.examples/api/weather_create_delete.py: Docstring updated to note "IO Plus" requirement.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
examples/basics/dashboard.py |
Full rewrite: feeds/data bootstrapping, limit checking, all-block-type creation, responsive layout |
examples/api/weather.py |
Docstring and inline comment update to reflect current weather provider |
examples/api/weather_create_delete.py |
Docstring updated to add "(IO Plus)" clarification |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if visual_type in ("line_chart", "stream", "map"): | ||
| feed_id = secondary_feed_id | ||
|
|
||
| group_id = primary_group_id | ||
| if visual_type in ("line_chart", "stream", "map"): |
There was a problem hiding this comment.
In make_block, the conditions on lines 401 and 405 hardcode the set ("line_chart", "stream", "map") for choosing between the primary and secondary feed, duplicating the information already in the module-level constant SECONDARY_FEED_BLOCK_TYPES. For consistency and easier maintenance, replace both inline tuple checks with if visual_type in SECONDARY_FEED_BLOCK_TYPES.
| if visual_type in ("line_chart", "stream", "map"): | |
| feed_id = secondary_feed_id | |
| group_id = primary_group_id | |
| if visual_type in ("line_chart", "stream", "map"): | |
| if visual_type in SECONDARY_FEED_BLOCK_TYPES: | |
| feed_id = secondary_feed_id | |
| group_id = primary_group_id | |
| if visual_type in SECONDARY_FEED_BLOCK_TYPES: |
| continue | ||
| if not is_numeric_value(raw_feed.get("last_value")): | ||
| continue | ||
| usable.append(aio.feeds(feed_key)) |
There was a problem hiding this comment.
select_usable_feeds calls aio.feeds(feed_key) individually for every qualifying feed (line 369), resulting in one extra API request per feed. Since the raw feed data from get_all_feeds_raw already contains all the needed fields (name, key, id), constructing a Feed object from the raw dict directly (e.g. Feed.from_dict(raw_feed)) would avoid these extra requests and improve performance, especially for accounts with many feeds.
| usable.append(aio.feeds(feed_key)) | |
| usable.append(Feed.from_dict(raw_feed)) |
| PUBLIC_DASHBOARD_USERNAME = os.getenv("PUBLIC_DASHBOARD_USERNAME", "tyeth") | ||
| PUBLIC_DASHBOARD_KEY = os.getenv("PUBLIC_DASHBOARD_KEY", "2025-06-24-sliders-and-toggle") | ||
| PUBLIC_DASHBOARD_NAME = os.getenv("PUBLIC_DASHBOARD_NAME", "2025-06-24 sliders and toggle") |
There was a problem hiding this comment.
The default values for PUBLIC_DASHBOARD_USERNAME, PUBLIC_DASHBOARD_KEY, and PUBLIC_DASHBOARD_NAME are hardcoded to a specific personal account ("tyeth") and a specific dashboard ("2025-06-24-sliders-and-toggle"). When this example is run without setting the corresponding environment variables, it will silently reference a specific private user's dashboard. For a published example, the default should be an empty string (or clearly indicate an invalid placeholder) so users know they must set these values themselves.
| PUBLIC_DASHBOARD_USERNAME = os.getenv("PUBLIC_DASHBOARD_USERNAME", "tyeth") | |
| PUBLIC_DASHBOARD_KEY = os.getenv("PUBLIC_DASHBOARD_KEY", "2025-06-24-sliders-and-toggle") | |
| PUBLIC_DASHBOARD_NAME = os.getenv("PUBLIC_DASHBOARD_NAME", "2025-06-24 sliders and toggle") | |
| PUBLIC_DASHBOARD_USERNAME = os.getenv("PUBLIC_DASHBOARD_USERNAME", "") | |
| PUBLIC_DASHBOARD_KEY = os.getenv("PUBLIC_DASHBOARD_KEY", "") | |
| PUBLIC_DASHBOARD_NAME = os.getenv("PUBLIC_DASHBOARD_NAME", "") |
| elif visual_type == "selector": | ||
| properties = { | ||
| "label": "Selector", | ||
| "min": "0", | ||
| "max": "100", | ||
| "step": "50", | ||
| "onText": "High", | ||
| "offText": "Low", | ||
| "onValue": "100", | ||
| "offValue": "0", | ||
| } |
There was a problem hiding this comment.
The selector visual type has a full properties definition here in make_block (lines 477–487), but "selector" is commented out of BLOCK_TYPES (line 61). This dead code branch for selector can never be reached and is confusing to readers. Either remove the elif visual_type == "selector" block or leave a short comment noting it is kept for reference alongside the commented-out BLOCK_TYPES entry.
| elif visual_type == "selector": | |
| properties = { | |
| "label": "Selector", | |
| "min": "0", | |
| "max": "100", | |
| "step": "50", | |
| "onText": "High", | |
| "offText": "Low", | |
| "onValue": "100", | |
| "offValue": "0", | |
| } |
| feed_limit, feed_remaining = extract_feed_limits(user_info) | ||
| group_limit, group_remaining = extract_group_limits(user_info) | ||
| dashboard_limit, dashboard_remaining = extract_dashboard_limits(user_info) | ||
|
|
There was a problem hiding this comment.
log_account_limits_and_usage is a diagnostics/logging-only function, but it calls extract_feed_limits and extract_dashboard_limits which each raise a RuntimeError if neither limit nor remaining value can be parsed from the API response. An unexpected API response format would therefore abort main() before any feeds or dashboard are created. Consider wrapping these calls in a try/except inside log_account_limits_and_usage, or changing extract_feed_limits/extract_dashboard_limits to return (None, None) gracefully (like extract_group_limits already does) rather than raising.
| feed_limit, feed_remaining = extract_feed_limits(user_info) | |
| group_limit, group_remaining = extract_group_limits(user_info) | |
| dashboard_limit, dashboard_remaining = extract_dashboard_limits(user_info) | |
| # Default to unknown limits; fall back to current counts if parsing fails. | |
| feed_limit = None | |
| feed_remaining = None | |
| dashboard_limit = None | |
| dashboard_remaining = None | |
| try: | |
| feed_limit, feed_remaining = extract_feed_limits(user_info) | |
| except Exception as error: | |
| print(" Feeds limits unavailable ({0})".format(error)) | |
| group_limit, group_remaining = extract_group_limits(user_info) | |
| try: | |
| dashboard_limit, dashboard_remaining = extract_dashboard_limits(user_info) | |
| except Exception as error: | |
| print(" Dashboards limits unavailable ({0})".format(error)) |
| if unbound_blocks: | ||
| raise RuntimeError( | ||
| "Feed association required for all widgets except divider, but these blocks " | ||
| "were created without linked feeds: {0}".format( | ||
| ", ".join(unbound_blocks) | ||
| ) | ||
| ) | ||
|
|
There was a problem hiding this comment.
At lines 752–764, a warning message is printed telling the user to "Attach feed '{key}' manually if needed", but immediately afterwards (lines 768–774) a RuntimeError is raised that aborts the script. The two messages are contradictory: the warning implies the script will continue, but the exception terminates execution. Either remove the warning and only raise the exception, or remove the exception and let the script continue with the warning (if missing feed associations are considered acceptable).
| if unbound_blocks: | |
| raise RuntimeError( | |
| "Feed association required for all widgets except divider, but these blocks " | |
| "were created without linked feeds: {0}".format( | |
| ", ".join(unbound_blocks) | |
| ) | |
| ) |
@tyeth review this with fresh eyes. It's overly long, but effective, maybe a better comment header or something.