Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .agents/skills/implement-widget-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,9 +354,9 @@ m = _compute_metrics(56) # row_h = widget h / number of rows
# "none" -> (0, 0)
# Content width (cw):
# cw = w - x_off - right_inset
# _card_insets(m, card_style, grayscale_levels) returns
# _card_insets(m, card_style, display_levels) returns
# (x_off, r_inset, bar_width). Derive lpad/rpad from the result:
# x_off, r_inset, _ = _card_insets(m, card_style, grayscale_levels)
# x_off, r_inset, _ = _card_insets(m, card_style, display_levels)
# lpad = m.padding if x_off == 0 else 0
# rpad = m.padding if r_inset == 0 else 0
# Icon circle left arc lands at x_off + lpad, NOT x_off + m.padding.
Expand All @@ -379,9 +379,9 @@ m = _compute_metrics(56) # row_h = widget h / number of rows
bounding box of non-white pixels.

4. **2-level display tests**: When the widget uses gray elements
(dividers, bars, left_bar), test the `grayscale_levels=2` path
(dividers, bars, left_bar), test the `display_levels=2` path
that widens them. Pass it via config:
`config = {**self._CONFIG, "grayscale_levels": 2}`.
`config = {**self._CONFIG, "display_levels": 2}`.

5. **Test each `card_style` variant**: For card-style widgets, test
all three styles (`"border"`, `"left_bar"`, `"none"`) in separate
Expand All @@ -395,7 +395,7 @@ m = _compute_metrics(56) # row_h = widget h / number of rows

```python
icon_stroke_w = (
m.border * 3 if grayscale_levels <= 2 else m.border
m.border * 3 if display_levels <= 2 else m.border
)
ring_y1 = icon_cy - icon_r + icon_stroke_w // 2 + 3
ring_y2 = icon_cy - m.icon_inner // 2 - 1
Expand Down
14 changes: 7 additions & 7 deletions .agents/skills/implement-widget/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ the context dict with `**_color_context()` so templates can use
`"left_bar"` → `(bar_w + m.padding, 0)`;
`"none"` → `(0, 0)`.
Content starts at `x_off`; content width = `w - x_off - right_inset`.
Always pass `grayscale_levels` from config.
Always pass `display_levels` from config.
- **`card_row`** expects all sizes from `WidgetMetrics` fields plus
`icon_svg` (pre-built SVG string, empty string for letter fallback)
and `letter` (single uppercase char, empty string when icon_svg is
Expand Down Expand Up @@ -179,7 +179,7 @@ white).
card_style=card_style,
radius=m.radius, border=m.border,
padding=m.padding, left_bar=m.left_bar,
grayscale_levels=grayscale_levels) -%}
display_levels=display_levels) -%}
{%- for row in rows -%}
{{ card_row(
x=x_off, y=row.y,
Expand Down Expand Up @@ -266,7 +266,7 @@ def _build_{widget_type}_context(
Args:
widget: Widget config dict with x, w, h, entities,
card_style, title.
config: DisplayConfig with states and grayscale_levels.
config: DisplayConfig with states and display_levels.

Returns:
Template context dict.
Expand All @@ -279,7 +279,7 @@ def _build_{widget_type}_context(
)
entities = widget.get("entities", [])
states = config.get("states", {})
grayscale_levels = config.get("grayscale_levels", 16)
display_levels = config.get("display_levels", 16)

n = len(entities)
if n == 0:
Expand All @@ -294,7 +294,7 @@ def _build_{widget_type}_context(
row_h = h // n
m = _compute_metrics(row_h)
x_off, r_inset, bar_width = _card_insets(
m, card_style, grayscale_levels
m, card_style, display_levels
)
lpad = m.padding if x_off == 0 else 0
rpad = m.padding if r_inset == 0 else 0
Expand Down Expand Up @@ -341,7 +341,7 @@ def _build_{widget_type}_context(
"w": w,
"h": h,
"card_style": card_style,
"grayscale_levels": grayscale_levels,
"display_levels": display_levels,
"rows": rows,
"row_h": row_h,
"lpad": lpad,
Expand Down Expand Up @@ -535,7 +535,7 @@ behavior.
```python
# Widen the outline stroke on 2-level displays.
icon_stroke_w = (
m.border * 3 if grayscale_levels <= 2 else m.border
m.border * 3 if display_levels <= 2 else m.border
)
```

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
conditions are met the tile renders inverted (solid black card,
white text/icon) as an e-ink "needs attention" signal.

### Fixed

- **Seeed reTerminal E1003** device preset now declares its correct
native landscape orientation, fixing a spurious 90° rotation that
produced a portrait PNG for the default landscape setup. Existing
dashboards are fixed automatically on upgrade via a config entry
migration; no manual reconfiguration is needed.

## [0.6.0] - 2026-07-07

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Supported device presets:
| TRMNL X | 1872 × 1404 | 16 |
| TRMNL RGB | 2560 × 1440 | 2 (black & white) |
| Seeed reTerminal E1001 | 800 × 480 | 4 |
| Seeed reTerminal E1003 | 1404 × 1872 | 16 |
| Seeed reTerminal E1003 | 1872 × 1404 | 16 |
| Custom | user-defined | 16 |

### Step 2 -- Image delivery
Expand Down
55 changes: 51 additions & 4 deletions custom_components/eink_dashboard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@

from .battery import resolve_battery_level
from .const import (
DEFAULT_DISPLAY_LEVELS,
DEFAULT_EXPOSURE,
DEFAULT_GRAYSCALE_LEVELS,
DEFAULT_HEIGHT,
DEFAULT_SATURATION,
DEFAULT_WIDTH,
Expand All @@ -51,6 +51,7 @@
NumberFormat,
TimeFormat,
WidgetType,
resolve_display,
)
from .http import EinkLayoutView, EinkPublicImageView
from .store import EinkDashboardStore
Expand Down Expand Up @@ -238,7 +239,7 @@ async def _build_display_config(
entry_id: Config entry ID present in ``hass.data[DOMAIN]``.

Returns:
Dict with ``width``, ``height``, ``grayscale_levels``,
Dict with ``width``, ``height``, ``display_levels``,
``number_format``, ``language``, ``first_weekday``,
``date_format``, ``time_format``, ``states``, and (when
battery data is available) ``device_battery_level`` and
Expand Down Expand Up @@ -267,8 +268,8 @@ async def _build_display_config(
config: dict[str, Any] = {
"width": entry.options.get("width", DEFAULT_WIDTH),
"height": entry.options.get("height", DEFAULT_HEIGHT),
"grayscale_levels": entry.options.get(
"grayscale_levels", DEFAULT_GRAYSCALE_LEVELS
"display_levels": entry.options.get(
"display_levels", DEFAULT_DISPLAY_LEVELS
),
"color_scheme": entry.options.get("color_scheme"),
"number_format": number_format,
Expand Down Expand Up @@ -731,6 +732,16 @@ async def async_migrate_entry(
in epaper-dithering; ``contrast`` is superseded by ``exposure``.
Both new keys default to 1.0 (no change).

Version 1.2 → 1.3: rename ``grayscale_levels`` to
``display_levels`` so the option name also fits future color
e-ink displays.

Version 1.3 → 1.4: recompute ``width``, ``height``, and
``rotation`` for ``reterminal_e1003`` entries. The device's
native orientation preset was previously wrong, so entries
created before the fix have a stale ``rotation`` baked in that
produces a 90°-rotated image.

Args:
hass: Home Assistant instance.
config_entry: The config entry to migrate.
Expand All @@ -755,6 +766,42 @@ async def async_migrate_entry(
minor_version=2,
)

if config_entry.minor_version == 2:
_LOGGER.debug(
"Migrating %s from minor version %d to 3",
config_entry.entry_id,
config_entry.minor_version,
)
new_options = dict(config_entry.options)
if "grayscale_levels" in new_options:
new_options["display_levels"] = new_options.pop("grayscale_levels")
hass.config_entries.async_update_entry(
config_entry,
options=new_options,
minor_version=3,
)

if config_entry.minor_version == 3:
_LOGGER.debug(
"Migrating %s from minor version %d to 4",
config_entry.entry_id,
config_entry.minor_version,
)
new_options = dict(config_entry.options)
if new_options.get("device_model") == "reterminal_e1003":
orientation = new_options.get("orientation", "landscape")
width, height, rotation, _preset = resolve_display(
"reterminal_e1003", orientation
)
new_options["width"] = width
new_options["height"] = height
new_options["rotation"] = rotation
hass.config_entries.async_update_entry(
config_entry,
options=new_options,
minor_version=4,
)

return True


Expand Down
Loading