Skip to content

Commit 53185c1

Browse files
cryptomilkclaude
andcommitted
fix(config): correct reTerminal E1003 native orientation to landscape
OpenDisplay firmware reports the panel's native buffer as landscape (1872x1404), not portrait. The old preset caused a spurious 90° rotation on the default landscape orientation, producing a portrait PNG that OpenDisplay's own fit logic then mangled trying to place back into its actual landscape buffer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 11b70e9 commit 53185c1

6 files changed

Lines changed: 163 additions & 9 deletions

File tree

CHANGELOG.md

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

17+
### Fixed
18+
19+
- **Seeed reTerminal E1003** device preset now declares its correct
20+
native landscape orientation, fixing a spurious 90° rotation that
21+
produced a portrait PNG for the default landscape setup. Existing
22+
dashboards are fixed automatically on upgrade via a config entry
23+
migration; no manual reconfiguration is needed.
24+
1725
## [0.6.0] - 2026-07-07
1826

1927
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Supported device presets:
8181
| TRMNL X | 1872 × 1404 | 16 |
8282
| TRMNL RGB | 2560 × 1440 | 2 (black & white) |
8383
| Seeed reTerminal E1001 | 800 × 480 | 4 |
84-
| Seeed reTerminal E1003 | 1404 × 1872 | 16 |
84+
| Seeed reTerminal E1003 | 1872 × 1404 | 16 |
8585
| Custom | user-defined | 16 |
8686

8787
### Step 2 -- Image delivery

custom_components/eink_dashboard/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
NumberFormat,
5252
TimeFormat,
5353
WidgetType,
54+
resolve_display,
5455
)
5556
from .http import EinkLayoutView, EinkPublicImageView
5657
from .store import EinkDashboardStore
@@ -735,6 +736,12 @@ async def async_migrate_entry(
735736
``display_levels`` so the option name also fits future color
736737
e-ink displays.
737738
739+
Version 1.3 → 1.4: recompute ``width``, ``height``, and
740+
``rotation`` for ``reterminal_e1003`` entries. The device's
741+
native orientation preset was previously wrong, so entries
742+
created before the fix have a stale ``rotation`` baked in that
743+
produces a 90°-rotated image.
744+
738745
Args:
739746
hass: Home Assistant instance.
740747
config_entry: The config entry to migrate.
@@ -774,6 +781,27 @@ async def async_migrate_entry(
774781
minor_version=3,
775782
)
776783

784+
if config_entry.minor_version == 3:
785+
_LOGGER.debug(
786+
"Migrating %s from minor version %d to 4",
787+
config_entry.entry_id,
788+
config_entry.minor_version,
789+
)
790+
new_options = dict(config_entry.options)
791+
if new_options.get("device_model") == "reterminal_e1003":
792+
orientation = new_options.get("orientation", "landscape")
793+
width, height, rotation, _preset = resolve_display(
794+
"reterminal_e1003", orientation
795+
)
796+
new_options["width"] = width
797+
new_options["height"] = height
798+
new_options["rotation"] = rotation
799+
hass.config_entries.async_update_entry(
800+
config_entry,
801+
options=new_options,
802+
minor_version=4,
803+
)
804+
777805
return True
778806

779807

custom_components/eink_dashboard/config_flow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ class EinkDashboardConfigFlow(ConfigFlow, domain=DOMAIN):
303303
"""Multi-step config flow for creating a new dashboard entry."""
304304

305305
VERSION = 1
306-
MINOR_VERSION = 3
306+
MINOR_VERSION = 4
307307

308308
def __init__(self) -> None:
309309
"""Initialise flow state."""

custom_components/eink_dashboard/const.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,12 @@ class DevicePreset:
183183
),
184184
"reterminal_e1003": DevicePreset(
185185
"reTerminal E1003",
186-
1404,
187186
1872,
187+
1404,
188188
16,
189189
False,
190190
"Seeed",
191+
native_landscape=True,
191192
integration_dithers=True,
192193
),
193194
"custom": DevicePreset(

tests/test_config_flow.py

Lines changed: 123 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,32 @@ async def test_reterminal_e1003_creates_entry(
202202
opts = result["options"]
203203
assert opts["optimize"] is False
204204
assert opts["display_levels"] == 16
205+
# Panel is native landscape (OpenDisplay firmware), so portrait
206+
# orientation is achieved by rotating the rendered canvas.
205207
assert opts["width"] == 1404
206208
assert opts["height"] == 1872
209+
assert opts["rotation"] == 90
210+
211+
async def test_reterminal_e1003_landscape_creates_entry(
212+
self, hass: HomeAssistant
213+
) -> None:
214+
# Native landscape device in landscape orientation (the
215+
# default): no rotation, and dimensions match the panel's
216+
# native landscape buffer.
217+
flow = await _make_config_flow(hass)
218+
result = await flow.async_step_user(
219+
{
220+
"name": "Office",
221+
"device_model": "reterminal_e1003",
222+
"orientation": "landscape",
223+
"update_interval": 60,
224+
}
225+
)
226+
227+
assert result["type"] is FlowResultType.CREATE_ENTRY
228+
opts = result["options"]
229+
assert opts["width"] == 1872
230+
assert opts["height"] == 1404
207231
assert opts["rotation"] == 0
208232

209233
async def test_screen_portion_shows_form_for_trmnl(
@@ -1808,7 +1832,7 @@ async def test_migration_removes_sharpness_contrast(
18081832
) -> None:
18091833
# Migration from minor_version<2 removes sharpness/contrast and
18101834
# adds exposure/saturation with their defaults. The entry then
1811-
# cascades through the 2->3 migration too, ending at 3.
1835+
# cascades through the 2->3 and 3->4 migrations too, ending at 4.
18121836
from custom_components.eink_dashboard import async_migrate_entry
18131837

18141838
entry = MockConfigEntry(
@@ -1827,7 +1851,7 @@ async def test_migration_removes_sharpness_contrast(
18271851
result = await async_migrate_entry(hass, entry)
18281852

18291853
assert result is True
1830-
assert entry.minor_version == 3
1854+
assert entry.minor_version == 4
18311855
assert "sharpness" not in entry.options
18321856
assert "contrast" not in entry.options
18331857
assert entry.options["exposure"] == 1.0
@@ -1863,7 +1887,8 @@ async def test_migration_renames_grayscale_levels(
18631887
self, hass: HomeAssistant
18641888
) -> None:
18651889
# Migration from minor_version<3 renames the stored
1866-
# grayscale_levels option key to display_levels.
1890+
# grayscale_levels option key to display_levels. The entry
1891+
# then cascades through the 3->4 migration too, ending at 4.
18671892
from custom_components.eink_dashboard import async_migrate_entry
18681893

18691894
entry = MockConfigEntry(
@@ -1882,21 +1907,113 @@ async def test_migration_renames_grayscale_levels(
18821907
result = await async_migrate_entry(hass, entry)
18831908

18841909
assert result is True
1885-
assert entry.minor_version == 3
1910+
assert entry.minor_version == 4
18861911
assert "grayscale_levels" not in entry.options
18871912
assert entry.options["display_levels"] == 4
18881913

1889-
async def test_migration_skipped_when_already_at_minor_version_3(
1914+
async def test_migration_fixes_reterminal_e1003_landscape_rotation(
18901915
self,
18911916
hass: HomeAssistant,
18921917
) -> None:
1893-
# Entries already at minor_version=3 are not migrated again.
1918+
# Migration from minor_version<4 recomputes width/height/
1919+
# rotation for reterminal_e1003 entries created under the old,
1920+
# buggy preset. A landscape entry had a stale rotation=90
1921+
# baked in; the fix clears it to 0.
18941922
from custom_components.eink_dashboard import async_migrate_entry
18951923

18961924
entry = MockConfigEntry(
18971925
domain=DOMAIN,
18981926
minor_version=3,
18991927
entry_id="test-entry",
1928+
options={
1929+
"device_model": "reterminal_e1003",
1930+
"orientation": "landscape",
1931+
"width": 1872,
1932+
"height": 1404,
1933+
"rotation": 90,
1934+
},
1935+
)
1936+
entry.add_to_hass(hass)
1937+
1938+
result = await async_migrate_entry(hass, entry)
1939+
1940+
assert result is True
1941+
assert entry.minor_version == 4
1942+
assert entry.options["width"] == 1872
1943+
assert entry.options["height"] == 1404
1944+
assert entry.options["rotation"] == 0
1945+
1946+
async def test_migration_fixes_reterminal_e1003_portrait_rotation(
1947+
self,
1948+
hass: HomeAssistant,
1949+
) -> None:
1950+
# A portrait reterminal_e1003 entry had a stale rotation=0
1951+
# baked in under the old preset; the fix sets it to 90.
1952+
from custom_components.eink_dashboard import async_migrate_entry
1953+
1954+
entry = MockConfigEntry(
1955+
domain=DOMAIN,
1956+
minor_version=3,
1957+
entry_id="test-entry",
1958+
options={
1959+
"device_model": "reterminal_e1003",
1960+
"orientation": "portrait",
1961+
"width": 1404,
1962+
"height": 1872,
1963+
"rotation": 0,
1964+
},
1965+
)
1966+
entry.add_to_hass(hass)
1967+
1968+
result = await async_migrate_entry(hass, entry)
1969+
1970+
assert result is True
1971+
assert entry.minor_version == 4
1972+
assert entry.options["width"] == 1404
1973+
assert entry.options["height"] == 1872
1974+
assert entry.options["rotation"] == 90
1975+
1976+
async def test_migration_skips_non_e1003_devices(
1977+
self,
1978+
hass: HomeAssistant,
1979+
) -> None:
1980+
# Entries for other device models must not have their
1981+
# width/height/rotation touched by the 3->4 migration.
1982+
from custom_components.eink_dashboard import async_migrate_entry
1983+
1984+
entry = MockConfigEntry(
1985+
domain=DOMAIN,
1986+
minor_version=3,
1987+
entry_id="test-entry",
1988+
options={
1989+
"device_model": "kindle_pw",
1990+
"orientation": "portrait",
1991+
"width": 758,
1992+
"height": 1024,
1993+
"rotation": 0,
1994+
},
1995+
)
1996+
entry.add_to_hass(hass)
1997+
1998+
result = await async_migrate_entry(hass, entry)
1999+
2000+
assert result is True
2001+
assert entry.minor_version == 4
2002+
assert entry.options["width"] == 758
2003+
assert entry.options["height"] == 1024
2004+
assert entry.options["rotation"] == 0
2005+
2006+
async def test_migration_skipped_when_already_at_minor_version_4(
2007+
self,
2008+
hass: HomeAssistant,
2009+
) -> None:
2010+
# Entries already at minor_version=4 are not migrated again.
2011+
from custom_components.eink_dashboard import async_migrate_entry
2012+
2013+
entry = MockConfigEntry(
2014+
domain=DOMAIN,
2015+
minor_version=4,
2016+
entry_id="test-entry",
19002017
options={
19012018
"update_interval": 60,
19022019
"exposure": 1.0,

0 commit comments

Comments
 (0)