led-ticker-crypto 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .env
@@ -0,0 +1,16 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.4.0
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+ - repo: local
9
+ hooks:
10
+ - id: pyright
11
+ name: pyright
12
+ entry: uv run pyright src
13
+ language: system
14
+ pass_filenames: false
15
+ always_run: true
16
+ stages: [pre-push]
@@ -0,0 +1,185 @@
1
+ # CLAUDE.md
2
+
3
+ Guidance for Claude Code when working in **led-ticker-crypto**, an external plugin for
4
+ [led-ticker](https://github.com/JamesAwesome/led-ticker).
5
+
6
+ `README.md` is the source of truth for the user-facing surface (config options, install,
7
+ coin-spec styles, rate limits). This file keeps the **load-bearing invariants** a contributor
8
+ must respect, plus navigation aids. When a fact here and the README disagree about *how a
9
+ feature works*, the README wins; this file is the source of truth for *how to keep it working*.
10
+
11
+ ## Overview
12
+
13
+ This plugin contributes, via the `led_ticker.plugins` entry point, a single widget:
14
+
15
+ - `crypto.coingecko` — live crypto price Container from the CoinGecko v3 API. Cycles one
16
+ `_CoinTicker` "story" per configured coin (the engine reads `feed_stories` via
17
+ `_expand_sources` on every pass). Shows symbol + price (adaptive precision) + 24h change,
18
+ trend-colored green/red/gray. One batched `/simple/price` fetch per update interval.
19
+
20
+ The entry-point name `crypto` is the plugin namespace, so the config `type` is
21
+ `crypto.coingecko` (see `register()` in `__init__.py`).
22
+
23
+ ## Commands
24
+
25
+ led-ticker is **not on PyPI**; it resolves from a sibling checkout via
26
+ `[tool.uv.sources] led-ticker = { path = "../led-ticker", editable = true }`. CI checks out
27
+ `led-ticker` next to this repo using a read-only deploy key (`LED_TICKER_DEPLOY_KEY`). The
28
+ rgbmatrix stub is vendored at the monorepo root (`tests/stubs/`) and put on the import path
29
+ by the **root** `pyproject.toml` (pytest `pythonpath` + pyright `extraPaths`), so the stub is
30
+ importable headless from the workspace root — no sibling `../led-ticker` path is involved.
31
+
32
+ ```bash
33
+ uv sync --extra dev # install deps (needs ../led-ticker checked out)
34
+ uv run pytest -q # full suite (asyncio_mode = "auto")
35
+ make lint # lint (monorepo root); or: uv run ruff check plugins/crypto
36
+ ```
37
+
38
+ Python **3.14+** only.
39
+
40
+ ## Package layout
41
+
42
+ ```
43
+ src/led_ticker_crypto/
44
+ __init__.py # register(api) → api.widget("coingecko")(CoinGeckoMonitor)
45
+ coingecko.py # CoinGeckoMonitor (Container): start(), update(), _build_coins,
46
+ # _resolve_symbols; _CoinTicker: per-coin story with draw()
47
+ _ticker_render.py # draw_price_ticker(): shared price-ticker renderer ported from core's
48
+ # coinbase widget; _format_price(); _ConstantColor; make_default_font_color
49
+ _colors.py # UP_TREND_COLOR / DOWN_TREND_COLOR / NEUTRAL_TREND_COLOR via lazy_palette
50
+ ```
51
+
52
+ `register(api)` (in `__init__.py`):
53
+
54
+ ```python
55
+ def register(api):
56
+ api.widget("coingecko")(CoinGeckoMonitor)
57
+ ```
58
+
59
+ ## Load-bearing invariants
60
+
61
+ Each rule must hold when modifying the named files.
62
+
63
+ **Import only the public surface** — every `led_ticker` import MUST come from `led_ticker.plugin`,
64
+ never `led_ticker.<internal>`. Enforced by `tests/test_import_purity.py`, which AST-walks every
65
+ source file (catches `from`-imports *and* `import led_ticker.x` forms, not just a text grep).
66
+ Intra-package imports (`from led_ticker_crypto._colors import …`) are fine. If you need a core
67
+ symbol that isn't on `led_ticker.plugin.__all__`, that's a core API change — raise it upstream,
68
+ don't reach around the surface.
69
+
70
+ **Python 3.14 / PEP 649** — no `from __future__ import annotations` anywhere (same rule as core).
71
+ Bare `tuple[int, int, int]` annotations are fine.
72
+
73
+ **`CoinGeckoMonitor` is a Container** — it exposes `feed_stories: list[_CoinTicker]` (one story
74
+ per coin). The engine reads `feed_stories` via `_expand_sources` on every pass through the
75
+ section, so live updates surface within at most one cycle. A single-coin config produces a
76
+ one-story container — the same code path, no special case. Never snapshot `feed_stories` into a
77
+ cycle iterator at section build time; that was the longboi stale-display pattern.
78
+
79
+ **`update()` uses ONE batched fetch with comma-joined ids** — multiple coin ids MUST be joined
80
+ into a single `ids` string (`ids=bitcoin,ethereum,dogecoin`). Passing a Python list would make
81
+ aiohttp emit repeated `ids=bitcoin&ids=ethereum` query parameters, which CoinGecko rejects for
82
+ more than one id. The comment in `update()` documents this constraint explicitly; do not change
83
+ the join to a list.
84
+
85
+ **Non-200 responses raise** — `update()` logs a warning (including `retry-after` if present),
86
+ then calls `response.raise_for_status()` so `run_monitor_loop`'s exponential backoff engages.
87
+ A 429 or any error body must NEVER be parsed as price data. This handles the CoinGecko free-tier
88
+ rate limit (~5 calls/min keyless) and any transient API error.
89
+
90
+ **`_format_price` adaptive precision** — coins ≥ $1 (or exactly $0) use the historical
91
+ `f"{value:,.4f}"` form (4 decimals, thousands-separated). Sub-dollar coins get extra decimals
92
+ computed as `min(12, max(4, 3 - floor(log10(abs(value)))))` so a coin at ~$0.0000046 renders
93
+ as `0.0000046` rather than collapsing to `0.0000`. Do not revert this to a fixed `.4f` format.
94
+
95
+ **`symbols` auto-lookup is unique-or-error** — `_resolve_symbols` queries the full
96
+ `/coins/list` response. For each requested symbol: exactly one match → `(symbol.upper(), id)`;
97
+ zero matches → `ValueError`; multiple matches → `ValueError` listing all candidate ids and
98
+ telling the user to set `symbol_id`/`symbol_ids` to disambiguate. Input order is preserved.
99
+ The `/coins/list` fetch only happens when `symbols` is non-empty; `symbol_ids`-only configs
100
+ skip it entirely.
101
+
102
+ **`_build_coins` assembles and deduplicates** — order is: legacy `symbol`+`symbol_id` → each
103
+ entry in `symbol_ids` → `symbols` (auto-resolved). Deduplication is by coin_id, keeping first
104
+ occurrence. Raises if the result is empty (the same message as `validate_config`).
105
+
106
+ **Optional `x-cg-demo-api-key` header** — `api_key` is sourced from the TOML field first, then
107
+ falls back to the `COINGECKO_API_KEY` environment variable (`os.getenv`). When non-empty, the
108
+ key is sent as `x-cg-demo-api-key` in all CoinGecko requests (both the `/coins/list` startup
109
+ fetch and the per-update `/simple/price` fetch). When empty, no key header is sent. No key is
110
+ required for a single low-frequency widget.
111
+
112
+ **`font_color` plumbing in `coingecko.py`** — `_CoinTicker.__attrs_post_init__` normalises the
113
+ raw config value: `None` → `make_default_font_color()` (yellow `_ConstantColor`); a plain
114
+ `Color` (not a `ColorProvider`) → `_ConstantColor(color)`. After `__attrs_post_init__`,
115
+ `self.font_color` is always a `ColorProvider`. `draw()` passes `frame_for("font_color")` so
116
+ animated providers (rainbow, shimmer) animate across engine ticks. The 24h change segment uses
117
+ `_get_change_color` and ignores `font_color`; this is intentional — change is always
118
+ trend-colored.
119
+
120
+ **One INFO log per successful `update()`** — the Container contract: a silent log stream after
121
+ startup signals the background task died. `update()` emits one INFO line per call showing
122
+ updated/total counts and the coin ids — never the raw API response.
123
+
124
+ **`start()` accepts `update_interval`** — this parameter is consumed by `start()` before the
125
+ `attrs` constructor, so it does NOT appear in `attrs.fields(cls)` and is filtered out of the
126
+ `**kwargs` forwarding. Any future parameter that belongs to the monitor lifecycle (not the
127
+ widget state) should follow the same pattern: accept in `start()`, don't pass through to
128
+ `cls(...)`.
129
+
130
+ **This is a faithful port of core's CoinGecko renderer** — `_ticker_render.draw_price_ticker`
131
+ was ported from `led_ticker.widgets.crypto.coinbase._draw_price_ticker` (coinbase was removed
132
+ from core; the renderer traveled with this plugin). Pixel-identity to the original was proven
133
+ during extraction (see PR history); `tools/compare_render.py` served that one-time validation
134
+ purpose and was retired after core's renderer was removed in led-ticker#188. Do not change
135
+ rendering logic in `_ticker_render.py` without confirming the diff is intentional.
136
+
137
+ **Port adaptations in `_ticker_render.py`** — these are deliberate, documented deviations from
138
+ how the original code was written that must NOT be reverted:
139
+
140
+ - `draw_text` is called in the public absolute-return form:
141
+ `cursor_pos = draw_text(canvas, font, text, x, y, color)` — not the core-internal
142
+ `cursor_pos += draw_text(...)` form. The result is pixel-identical for plain text.
143
+ - `compute_cursor`'s `center` parameter is keyword-only at the call site.
144
+ - The default `font_color` is yellow `(255, 255, 0)`, matching core's `DEFAULT_COLOR`.
145
+ - `_ConstantColor` is a local reproduction of core's private class (which is not on the public
146
+ surface). It wraps a plain `Color` so a `font_color = [r,g,b]` config routes through the same
147
+ `color_for` interface as effect providers.
148
+
149
+ **Font constants** — `FONT_LABEL` (`7x13`) and `FONT_DELTA` (`6x10`) are resolved once at import
150
+ via `resolve_font`. `FONT_VALUE` and `FONT_VALUE_SMALL` alias `FONT_DEFAULT` / `FONT_SMALL` from
151
+ `led_ticker.plugin` — these are BDF faces (`6x12` / `5x8`). The price font auto-downgrades to
152
+ `FONT_VALUE_SMALL` when the price string exceeds 10 characters (long decimals and large values).
153
+
154
+ **Trend palette is lazy** — `_colors.py` uses `colors.lazy_palette` (PEP 562 `__getattr__`),
155
+ so importing the module is a no-op against the rgbmatrix `graphics` library. In-module code
156
+ (e.g. inside `_ticker_render.py`) accesses them as module-level names
157
+ (`UP_TREND_COLOR`, `DOWN_TREND_COLOR`, `NEUTRAL_TREND_COLOR`), which triggers the lazy resolver
158
+ on first access. Do not call `_trend_palette(name)` directly from outside `_colors.py`.
159
+
160
+ ## Tests / CI
161
+
162
+ `uv run pytest -q` runs the suite (`tests/`):
163
+
164
+ - `test_import_purity.py` — AST tripwire (public-surface-only). Treat a failure as a contract
165
+ violation, not a test to relax.
166
+ - `test_smoke.py` — loads the plugin through led-ticker's real plugin loader and asserts
167
+ `crypto.coingecko` registers under the `crypto` namespace (entry-point wiring guard).
168
+ - `test_coingecko.py` — behavior coverage: price formatting, change coloring, `font_color`
169
+ normalization, `draw()` routing, `update()` logging contract, multi-coin batching, symbol
170
+ resolution, rate-limit error handling.
171
+
172
+ `tools/compare_render.py` — standalone comparison tool used during extraction to assert pixel
173
+ identity between the ported renderer and core's original. Retired after core's renderer was
174
+ removed in led-ticker#188; see PR history for the comparison baseline.
175
+
176
+ CI is the monorepo's single root path-filtered per-member matrix (`.github/workflows/ci.yml`):
177
+ it checks out led-ticker as a sibling (deploy key), Python 3.14, `uv sync --extra dev`, then
178
+ runs ruff check, ruff format --check, pyright, and pytest for the changed member.
179
+
180
+ ## Adding to the plugin
181
+
182
+ Register the class in `register()` in `__init__.py` (`api.widget`); it becomes `crypto.<name>`.
183
+ Import any core dependency from `led_ticker.plugin` only, and keep the import-purity test green.
184
+ If the new widget shares the price-ticker layout (symbol + price + change), reuse
185
+ `_ticker_render.draw_price_ticker` rather than duplicating the draw logic.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James Awesome
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,18 @@
1
+ .PHONY: dev test lint format typecheck
2
+
3
+ dev: ## Install dev deps + pre-commit hooks
4
+ uv sync --extra dev
5
+ uv run pre-commit install
6
+ uv run pre-commit install --hook-type pre-push
7
+
8
+ test: ## Run tests with coverage
9
+ uv run pytest --cov=src --cov-report=term-missing
10
+
11
+ lint: ## Ruff lint
12
+ uv run ruff check src tests
13
+
14
+ format: ## Ruff format
15
+ uv run ruff format src tests
16
+
17
+ typecheck: ## Pyright
18
+ uv run pyright src
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: led-ticker-crypto
3
+ Version: 0.2.0
4
+ Summary: Crypto-price widgets for led-ticker (CoinGecko).
5
+ Project-URL: Homepage, https://docs.ledticker.dev
6
+ Project-URL: Repository, https://github.com/JamesAwesome/led-ticker-plugins
7
+ Project-URL: Issues, https://github.com/JamesAwesome/led-ticker-plugins/issues
8
+ Author-email: James Awesome <james@morelli.nyc>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Multimedia :: Graphics
16
+ Requires-Python: >=3.14
17
+ Requires-Dist: aiohttp
18
+ Requires-Dist: led-ticker-core>=2.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: pre-commit>=4.0; extra == 'dev'
21
+ Requires-Dist: pyright>=1.1; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
23
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
24
+ Requires-Dist: pytest>=8.0; extra == 'dev'
25
+ Requires-Dist: ruff>=0.4; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # led-ticker-crypto
29
+
30
+ A cryptocurrency price ticker **plugin** for [led-ticker](https://github.com/JamesAwesome/led-ticker), backed by the free CoinGecko v3 API. It contributes a `crypto.coingecko` **Container widget** that cycles one scrolling price line per configured coin — configure one coin for a single ticker, or a list to cycle through several.
31
+
32
+ Each line shows the coin symbol, current price (adaptive precision — sub-dollar tokens never collapse to `0.0000`), and 24-hour percent change. The change value is trend-colored: green for positive, red for negative, gray for neutral — readable at a glance on any panel.
33
+
34
+ ![crypto.coingecko cycling BTC, ETH, and SHIB (note the sub-cent SHIB price)](docs/crypto-coingecko.gif)
35
+
36
+ ## Prerequisites
37
+
38
+ - A working [led-ticker](https://github.com/JamesAwesome/led-ticker) install.
39
+ - Internet access on the Pi (the widget calls the CoinGecko public API).
40
+
41
+ ## Install
42
+
43
+ The widget auto-registers via the `led_ticker.plugins` entry point — once the package is installed, no `[plugins]` config change is needed.
44
+
45
+ **Into a containerized led-ticker (recommended):** add this package to `config/requirements-plugins.txt` (copy it from `config/requirements-plugins.example.txt`, which already lists it), then rebuild:
46
+
47
+ ```bash
48
+ # in your led-ticker checkout
49
+ cp config/requirements-plugins.example.txt config/requirements-plugins.txt
50
+ docker compose up -d --build
51
+ ```
52
+
53
+ That example file lists every first-party plugin — trim the live copy to just the ones you want. The crypto line is:
54
+
55
+ ```text
56
+ git+https://github.com/JamesAwesome/led-ticker-crypto.git@main
57
+ ```
58
+
59
+ **Standalone (a venv that already has led-ticker):**
60
+
61
+ ```bash
62
+ pip install "git+https://github.com/JamesAwesome/led-ticker-crypto.git@main"
63
+ ```
64
+
65
+ led-ticker isn't on PyPI, so this path only works where led-ticker is already installed. See the led-ticker [Plugins docs](https://docs.ledticker.dev/plugins/) for the constraint-based install the Docker image uses.
66
+
67
+ ## Configuration
68
+
69
+ Reference the widget in a playlist section by `type = "crypto.coingecko"`. Three coin-spec styles are supported — choose the one that fits your workflow:
70
+
71
+ ### Single coin (legacy style)
72
+
73
+ ```toml
74
+ [[playlist.section.widget]]
75
+ type = "crypto.coingecko"
76
+ symbol = "BTC"
77
+ symbol_id = "bitcoin"
78
+ currency = "USD"
79
+ ```
80
+
81
+ ### Multiple coins by explicit CoinGecko id
82
+
83
+ ```toml
84
+ [[playlist.section.widget]]
85
+ type = "crypto.coingecko"
86
+ symbol_ids = ["bitcoin", "ethereum", "dogecoin"]
87
+ currency = "USD"
88
+ ```
89
+
90
+ Each id in `symbol_ids` is also used as the on-panel label (uppercased). Use `symbol_ids` when you know the exact CoinGecko ids and want to skip the startup lookup.
91
+
92
+ ### Multiple coins by ticker symbol (auto-resolved)
93
+
94
+ ```toml
95
+ [[playlist.section.widget]]
96
+ type = "crypto.coingecko"
97
+ symbols = ["BTC", "ETH", "SOL"]
98
+ currency = "USD"
99
+ ```
100
+
101
+ `symbols` are resolved at startup against CoinGecko's `/coins/list` endpoint. Resolution is **unique-or-error**: if a symbol matches more than one CoinGecko id, the widget fails at startup and lists the candidate ids — use `symbol_id` or `symbol_ids` to disambiguate.
102
+
103
+ You can combine all three styles in one widget; duplicates (by coin id) are silently dropped, keeping the first occurrence.
104
+
105
+ New to led-ticker configs? The [first-config tutorial](https://docs.ledticker.dev/tutorial/02-first-config/) walks through the overall structure — the blocks above show just the crypto-specific keys.
106
+
107
+ ### Finding `symbol_id`
108
+
109
+ `symbol_id` is CoinGecko's internal coin identifier (e.g. `"bitcoin"`, `"ethereum"`, `"solana"`, `"dogecoin"`). It's the `id` field in CoinGecko's `/coins/list` endpoint and appears in a coin's page URL: `coingecko.com/en/coins/<id>`.
110
+
111
+ Use `symbol_id` (or `symbol_ids`) when:
112
+ - You want to skip the startup `/coins/list` lookup that `symbols` triggers.
113
+ - A symbol is ambiguous — the startup error lists the candidates and tells you which ids to use.
114
+
115
+ ### Options
116
+
117
+ | Option | Type | Default | Description |
118
+ |--------|------|---------|-------------|
119
+ | `symbol` | string | — | Single-coin label shown on the panel (e.g. `"BTC"`). Requires `symbol_id`. |
120
+ | `symbol_id` | string | — | Single-coin CoinGecko id (e.g. `"bitcoin"`). Requires `symbol`. |
121
+ | `symbols` | list of strings | — | Ticker symbols auto-resolved to CoinGecko ids at startup (e.g. `["BTC", "ETH"]`). Unique-or-error. |
122
+ | `symbol_ids` | list of strings | — | CoinGecko ids used directly, uppercased as panel labels (e.g. `["bitcoin", "ethereum"]`). |
123
+ | `currency` | string | `"USD"` | Fiat currency code (e.g. `"USD"`, `"EUR"`). |
124
+ | `update_interval` | int | `300` | Seconds between CoinGecko fetches (5 min default). |
125
+ | `center` | bool | `true` | Center the ticker on the canvas when it fits; scroll when it overflows. |
126
+ | `padding` | int | `6` | Horizontal spacing (logical px) between the symbol, price, and change segments. |
127
+ | `hold_time` | float | `0.0` | Seconds to hold each coin's ticker before the engine cycles to the next. |
128
+ | `bg_color` | `[r,g,b]` | none | Background fill behind the ticker. |
129
+ | `font_color` | `[r,g,b]` / string / table | yellow `(255,255,0)` | Color for the symbol and price. Accepts any led-ticker color provider (e.g. `"rainbow"`, `{style="shimmer", ...}`). The 24h change color is always trend-colored and ignores this field. |
130
+
131
+ At least one of `symbol`+`symbol_id`, `symbols`, or `symbol_ids` must be specified — the widget fails at config validation otherwise.
132
+
133
+ ### Rate limits & API key
134
+
135
+ CoinGecko's keyless free tier allows roughly **5 requests per minute**. For a single low-frequency widget at the default 5-minute interval this is plenty; if you add more coins or shorten `update_interval`, you may hit HTTP 429. When that happens the widget logs the rate-limit clearly and lets its monitor loop back off — it will not show stale garbage.
136
+
137
+ To raise the limit, create a free account at [coingecko.com](https://www.coingecko.com) and get a **Demo API key** (no credit card required). Supply it via the `COINGECKO_API_KEY` environment variable (the only supported path — config-file secrets are intentionally not accepted):
138
+
139
+ ```bash
140
+ export COINGECKO_API_KEY="CG-xxxxxxxxxxxxxxxxxxxx"
141
+ ```
142
+
143
+ The key is sent as the `x-cg-demo-api-key` request header.
144
+
145
+ ## Development
146
+
147
+ led-ticker isn't on PyPI, so this plugin resolves it from a sibling checkout. Clone both side by side:
148
+
149
+ ```
150
+ ~/projects/.../led-ticker
151
+ ~/projects/.../led-ticker-crypto
152
+ ```
153
+
154
+ ```bash
155
+ uv sync --extra dev # resolves led-ticker from ../led-ticker
156
+ uv run pytest -q
157
+ uv run ruff check src tests
158
+ ```
159
+
160
+ > **Note:** led-ticker's `graphics` surface works headless via its bundled stub, but the full `RGBMatrix`/canvas test stub lives in led-ticker's `tests/stubs/` and isn't shipped. This repo's tests put it on the path via `pyproject.toml`'s `[tool.pytest.ini_options] pythonpath = ["../led-ticker/tests/stubs"]`.
161
+
162
+ The plugin imports only the public `led_ticker.plugin` surface — `tests/test_import_purity.py` enforces it.
163
+
164
+ ## Links
165
+
166
+ - [led-ticker](https://github.com/JamesAwesome/led-ticker) — the core project
167
+ - [Docs site](https://docs.ledticker.dev) · [Plugin system](https://docs.ledticker.dev/plugins/)
@@ -0,0 +1,140 @@
1
+ # led-ticker-crypto
2
+
3
+ A cryptocurrency price ticker **plugin** for [led-ticker](https://github.com/JamesAwesome/led-ticker), backed by the free CoinGecko v3 API. It contributes a `crypto.coingecko` **Container widget** that cycles one scrolling price line per configured coin — configure one coin for a single ticker, or a list to cycle through several.
4
+
5
+ Each line shows the coin symbol, current price (adaptive precision — sub-dollar tokens never collapse to `0.0000`), and 24-hour percent change. The change value is trend-colored: green for positive, red for negative, gray for neutral — readable at a glance on any panel.
6
+
7
+ ![crypto.coingecko cycling BTC, ETH, and SHIB (note the sub-cent SHIB price)](docs/crypto-coingecko.gif)
8
+
9
+ ## Prerequisites
10
+
11
+ - A working [led-ticker](https://github.com/JamesAwesome/led-ticker) install.
12
+ - Internet access on the Pi (the widget calls the CoinGecko public API).
13
+
14
+ ## Install
15
+
16
+ The widget auto-registers via the `led_ticker.plugins` entry point — once the package is installed, no `[plugins]` config change is needed.
17
+
18
+ **Into a containerized led-ticker (recommended):** add this package to `config/requirements-plugins.txt` (copy it from `config/requirements-plugins.example.txt`, which already lists it), then rebuild:
19
+
20
+ ```bash
21
+ # in your led-ticker checkout
22
+ cp config/requirements-plugins.example.txt config/requirements-plugins.txt
23
+ docker compose up -d --build
24
+ ```
25
+
26
+ That example file lists every first-party plugin — trim the live copy to just the ones you want. The crypto line is:
27
+
28
+ ```text
29
+ git+https://github.com/JamesAwesome/led-ticker-crypto.git@main
30
+ ```
31
+
32
+ **Standalone (a venv that already has led-ticker):**
33
+
34
+ ```bash
35
+ pip install "git+https://github.com/JamesAwesome/led-ticker-crypto.git@main"
36
+ ```
37
+
38
+ led-ticker isn't on PyPI, so this path only works where led-ticker is already installed. See the led-ticker [Plugins docs](https://docs.ledticker.dev/plugins/) for the constraint-based install the Docker image uses.
39
+
40
+ ## Configuration
41
+
42
+ Reference the widget in a playlist section by `type = "crypto.coingecko"`. Three coin-spec styles are supported — choose the one that fits your workflow:
43
+
44
+ ### Single coin (legacy style)
45
+
46
+ ```toml
47
+ [[playlist.section.widget]]
48
+ type = "crypto.coingecko"
49
+ symbol = "BTC"
50
+ symbol_id = "bitcoin"
51
+ currency = "USD"
52
+ ```
53
+
54
+ ### Multiple coins by explicit CoinGecko id
55
+
56
+ ```toml
57
+ [[playlist.section.widget]]
58
+ type = "crypto.coingecko"
59
+ symbol_ids = ["bitcoin", "ethereum", "dogecoin"]
60
+ currency = "USD"
61
+ ```
62
+
63
+ Each id in `symbol_ids` is also used as the on-panel label (uppercased). Use `symbol_ids` when you know the exact CoinGecko ids and want to skip the startup lookup.
64
+
65
+ ### Multiple coins by ticker symbol (auto-resolved)
66
+
67
+ ```toml
68
+ [[playlist.section.widget]]
69
+ type = "crypto.coingecko"
70
+ symbols = ["BTC", "ETH", "SOL"]
71
+ currency = "USD"
72
+ ```
73
+
74
+ `symbols` are resolved at startup against CoinGecko's `/coins/list` endpoint. Resolution is **unique-or-error**: if a symbol matches more than one CoinGecko id, the widget fails at startup and lists the candidate ids — use `symbol_id` or `symbol_ids` to disambiguate.
75
+
76
+ You can combine all three styles in one widget; duplicates (by coin id) are silently dropped, keeping the first occurrence.
77
+
78
+ New to led-ticker configs? The [first-config tutorial](https://docs.ledticker.dev/tutorial/02-first-config/) walks through the overall structure — the blocks above show just the crypto-specific keys.
79
+
80
+ ### Finding `symbol_id`
81
+
82
+ `symbol_id` is CoinGecko's internal coin identifier (e.g. `"bitcoin"`, `"ethereum"`, `"solana"`, `"dogecoin"`). It's the `id` field in CoinGecko's `/coins/list` endpoint and appears in a coin's page URL: `coingecko.com/en/coins/<id>`.
83
+
84
+ Use `symbol_id` (or `symbol_ids`) when:
85
+ - You want to skip the startup `/coins/list` lookup that `symbols` triggers.
86
+ - A symbol is ambiguous — the startup error lists the candidates and tells you which ids to use.
87
+
88
+ ### Options
89
+
90
+ | Option | Type | Default | Description |
91
+ |--------|------|---------|-------------|
92
+ | `symbol` | string | — | Single-coin label shown on the panel (e.g. `"BTC"`). Requires `symbol_id`. |
93
+ | `symbol_id` | string | — | Single-coin CoinGecko id (e.g. `"bitcoin"`). Requires `symbol`. |
94
+ | `symbols` | list of strings | — | Ticker symbols auto-resolved to CoinGecko ids at startup (e.g. `["BTC", "ETH"]`). Unique-or-error. |
95
+ | `symbol_ids` | list of strings | — | CoinGecko ids used directly, uppercased as panel labels (e.g. `["bitcoin", "ethereum"]`). |
96
+ | `currency` | string | `"USD"` | Fiat currency code (e.g. `"USD"`, `"EUR"`). |
97
+ | `update_interval` | int | `300` | Seconds between CoinGecko fetches (5 min default). |
98
+ | `center` | bool | `true` | Center the ticker on the canvas when it fits; scroll when it overflows. |
99
+ | `padding` | int | `6` | Horizontal spacing (logical px) between the symbol, price, and change segments. |
100
+ | `hold_time` | float | `0.0` | Seconds to hold each coin's ticker before the engine cycles to the next. |
101
+ | `bg_color` | `[r,g,b]` | none | Background fill behind the ticker. |
102
+ | `font_color` | `[r,g,b]` / string / table | yellow `(255,255,0)` | Color for the symbol and price. Accepts any led-ticker color provider (e.g. `"rainbow"`, `{style="shimmer", ...}`). The 24h change color is always trend-colored and ignores this field. |
103
+
104
+ At least one of `symbol`+`symbol_id`, `symbols`, or `symbol_ids` must be specified — the widget fails at config validation otherwise.
105
+
106
+ ### Rate limits & API key
107
+
108
+ CoinGecko's keyless free tier allows roughly **5 requests per minute**. For a single low-frequency widget at the default 5-minute interval this is plenty; if you add more coins or shorten `update_interval`, you may hit HTTP 429. When that happens the widget logs the rate-limit clearly and lets its monitor loop back off — it will not show stale garbage.
109
+
110
+ To raise the limit, create a free account at [coingecko.com](https://www.coingecko.com) and get a **Demo API key** (no credit card required). Supply it via the `COINGECKO_API_KEY` environment variable (the only supported path — config-file secrets are intentionally not accepted):
111
+
112
+ ```bash
113
+ export COINGECKO_API_KEY="CG-xxxxxxxxxxxxxxxxxxxx"
114
+ ```
115
+
116
+ The key is sent as the `x-cg-demo-api-key` request header.
117
+
118
+ ## Development
119
+
120
+ led-ticker isn't on PyPI, so this plugin resolves it from a sibling checkout. Clone both side by side:
121
+
122
+ ```
123
+ ~/projects/.../led-ticker
124
+ ~/projects/.../led-ticker-crypto
125
+ ```
126
+
127
+ ```bash
128
+ uv sync --extra dev # resolves led-ticker from ../led-ticker
129
+ uv run pytest -q
130
+ uv run ruff check src tests
131
+ ```
132
+
133
+ > **Note:** led-ticker's `graphics` surface works headless via its bundled stub, but the full `RGBMatrix`/canvas test stub lives in led-ticker's `tests/stubs/` and isn't shipped. This repo's tests put it on the path via `pyproject.toml`'s `[tool.pytest.ini_options] pythonpath = ["../led-ticker/tests/stubs"]`.
134
+
135
+ The plugin imports only the public `led_ticker.plugin` surface — `tests/test_import_purity.py` enforces it.
136
+
137
+ ## Links
138
+
139
+ - [led-ticker](https://github.com/JamesAwesome/led-ticker) — the core project
140
+ - [Docs site](https://docs.ledticker.dev) · [Plugin system](https://docs.ledticker.dev/plugins/)
@@ -0,0 +1,20 @@
1
+ # render-duration: 18
2
+ # Demo config for the crypto.coingecko widget (multi-coin ticker).
3
+ # Explicit symbol_ids for a deterministic render; shiba-inu showcases the
4
+ # adaptive sub-cent price formatting.
5
+ [display]
6
+ rows = 16
7
+ cols = 32
8
+ chain_length = 5
9
+ default_scale = 1
10
+ brightness = 60
11
+
12
+ [[playlist.section]]
13
+ mode = "swap"
14
+ loop_count = 1
15
+ hold_time = 3.0
16
+
17
+ [[playlist.section.widget]]
18
+ type = "crypto.coingecko"
19
+ symbol_ids = ["bitcoin", "ethereum", "shiba-inu"]
20
+ currency = "USD"