From 2065cee43daa6cdf2bcea5a8a8d244bac74c5a79 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 18:16:34 -0400 Subject: [PATCH 1/6] fix(weather): stop the Vegas ticker spending a display width on current conditions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `get_vegas_content()` handed the ticker the same image the full-screen renderer produces. That layout anchors the condition, temperature and high/low to the *right edge of the panel* and distributes the metrics bar evenly across the whole width — correct for a screen you look at, wrong for a tile that scrolls past. Measured on a live 512x64 device, the three weather modes contributed 1536px every rebuild and `auto_trim` reclaimed only 14% of it, because the ink genuinely reaches both edges — there is no blank margin to crop. For comparison, other plugins hand the ticker tiles of 110-145px: [ledmatrix-weather] Native: SUCCESS - 3 images, 1536px total width [ledmatrix-weather] Trimmed native content: 1536px -> 1318px (14% reclaimed) The ticker now gets its own left-to-right layout for current conditions, measured and sized to exactly what it draws: icon, then temperature over high/low, then condition over the metrics. 250px instead of 512px on a 512-wide panel — half a display width per weather cycle. With a guard, because the compact arrangement is not universally narrower. Fixed-size bitmap fonts do not shrink with the display, so at 128x32 laying those columns side by side comes to 198px against a 128px screen. When the compact tile would be at least as wide as the panel it falls back to the full-screen tile, so a narrow display reclaims nothing rather than paying extra. The full-screen renderer is untouched and still serves the normal rotation slot; only the Vegas path changed. Core harness passes for all five weather modes at 128x32, 256x64 and 512x64. Hourly and daily are left alone for now: they are genuinely multi-column and fill their width, so the same treatment needs its own look. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 --- plugins.json | 4 +- plugins/ledmatrix-weather/manager.py | 101 +++++++++++++++++++++++- plugins/ledmatrix-weather/manifest.json | 14 +++- 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/plugins.json b/plugins.json index 926231b4..5b032881 100644 --- a/plugins.json +++ b/plugins.json @@ -928,10 +928,10 @@ "repo": "https://github.com/ChuckBuilds/ledmatrix-plugins", "branch": "main", "plugin_path": "plugins/ledmatrix-weather", - "latest_version": "2.6.2", + "latest_version": "2.6.3", "stars": 0, "downloads": 0, - "last_updated": "2026-07-19", + "last_updated": "2026-08-05", "verified": true, "screenshot": "" }, diff --git a/plugins/ledmatrix-weather/manager.py b/plugins/ledmatrix-weather/manager.py index 2a4c2111..51b8f447 100644 --- a/plugins/ledmatrix-weather/manager.py +++ b/plugins/ledmatrix-weather/manager.py @@ -1814,6 +1814,105 @@ def reset_cycle_state(self) -> None: if fetcher is not None: fetcher.reset_loop() + def _render_current_weather_vegas_image(self) -> Optional[Image.Image]: + """Current conditions sized to its content, for the Vegas ticker. + + The full-screen renderer anchors the condition, temperature and + high/low to the *right edge of the panel* and spreads the metrics bar + evenly across the whole width. That is right for a screen you look at, + but as a ticker tile it means one display width per weather mode -- + three of them, 1536px, of which auto_trim could only reclaim 14% + because the ink genuinely reaches both edges. Other plugins hand the + ticker tiles of 110-145px. + + So the ticker gets its own left-to-right layout, measured and sized to + exactly what it draws. The full-screen version is untouched and still + serves the normal rotation slot. + """ + try: + height = self.display_manager.matrix.height + layout = self._get_layout() + + temp = int(self.weather_data['main']['temp']) + condition = self.weather_data['weather'][0]['main'] + icon_code = self.weather_data['weather'][0]['icon'] + humidity = self.weather_data['main']['humidity'] + wind_speed = self.weather_data['wind'].get('speed', 0) + wind_deg = self.weather_data['wind'].get('deg', 0) + uv_index = self.weather_data['main'].get('uvi', 0) + temp_high = int(self.weather_data['main']['temp_max']) + temp_low = int(self.weather_data['main']['temp_min']) + + temp_font = self.display_manager.small_font + small_font = self.display_manager.small_font + tiny_font = self.display_manager.extra_small_font + + gap = max(3, round(4 * (height / 32.0))) + icon_size = layout['current_icon_size'] + + # Column 2 stacks temperature over high/low; column 3 stacks the + # condition over the metrics. Width is the widest line in each. + temp_text = f"{temp}°" + high_low_text = f"{temp_low}°/{temp_high}°" + metrics_text = (f"UV:{uv_index:.0f} H:{humidity}% " + f"W:{wind_speed:.0f}{self._get_wind_direction(wind_deg)}") + + measure = ImageDraw.Draw(Image.new('RGB', (1, 1))) + + def text_w(text, font): + return int(measure.textlength(text, font=font)) + + temp_w = text_w(temp_text, temp_font) + high_low_w = text_w(high_low_text, small_font) + condition_w = text_w(condition, small_font) + metrics_w = text_w(metrics_text, tiny_font) + + col2_w = max(temp_w, high_low_w) + col3_w = max(condition_w, metrics_w) + total_width = icon_size + gap + col2_w + gap + col3_w + gap + + # On a narrow panel this side-by-side arrangement is *wider* than + # the full-screen tile it replaces (198px against 128px at + # 128x32), because fixed-size bitmap fonts do not shrink with the + # display. Reclaiming nothing is fine; costing extra is not. + if total_width >= self.display_manager.matrix.width: + self.logger.debug( + "[Weather Vegas] Compact current tile would be %dpx on a " + "%dpx panel; using the full-screen layout instead", + total_width, self.display_manager.matrix.width) + return self._render_current_weather_image() + + img = Image.new('RGB', (total_width, height), (0, 0, 0)) + draw = ImageDraw.Draw(img) + + WeatherIcons.draw_weather_icon( + img, icon_code, 0, max(0, (height - icon_size) // 2), size=icon_size) + + # Two text rows, vertically centred as a pair. + row_h = 8 + top_y = max(0, (height - (row_h * 2 + 2)) // 2) + bottom_y = top_y + row_h + 2 + + x = icon_size + gap + draw.text((x, top_y), temp_text, font=temp_font, + fill=self.COLORS['highlight']) + draw.text((x, bottom_y), high_low_text, font=small_font, + fill=self.COLORS['dim']) + + x += col2_w + gap + draw.text((x, top_y), condition, font=small_font, + fill=self.COLORS['text']) + draw.text((x, bottom_y), metrics_text, font=tiny_font, + fill=self.COLORS['dim']) + + return img + + except Exception as e: + self.logger.error("Error rendering compact current weather: %s", e, + exc_info=True) + # Fall back to the full-screen tile rather than dropping the mode. + return self._render_current_weather_image() + def get_vegas_content(self): """Return images for all enabled weather display modes.""" if not self.weather_data: @@ -1822,7 +1921,7 @@ def get_vegas_content(self): images = [] if self.show_current: - img = self._render_current_weather_image() + img = self._render_current_weather_vegas_image() if img: images.append(img) diff --git a/plugins/ledmatrix-weather/manifest.json b/plugins/ledmatrix-weather/manifest.json index 6825e388..4c036f6e 100644 --- a/plugins/ledmatrix-weather/manifest.json +++ b/plugins/ledmatrix-weather/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-weather", "name": "Weather Display", - "version": "2.6.2", + "version": "2.6.3", "author": "ChuckBuilds", "class_name": "WeatherPlugin", "update_interval": 60, @@ -26,6 +26,12 @@ "radar" ], "versions": [ + { + "version": "2.6.3", + "released": "2026-08-05", + "notes": "Give the Vegas ticker a content-sized current-conditions tile. The full-screen renderer anchors condition, temperature and high/low to the right edge of the panel and spreads the metrics bar across the whole width, so reused as a ticker tile it costs a full display width -- three weather modes came to 1536px, of which auto_trim could reclaim only 14% because the ink genuinely reaches both edges, while other plugins hand the ticker tiles of 110-145px. The ticker now gets its own left-to-right layout measured to exactly what it draws: 250px instead of 512px on a 512-wide panel. On narrow panels, where fixed-size bitmap fonts make the side-by-side arrangement wider than the screen, it falls back to the full-screen tile rather than costing more than it saves. The normal rotation slot is unchanged.", + "ledmatrix_min_version": "2.0.0" + }, { "released": "2026-07-21", "version": "2.6.2", @@ -41,7 +47,7 @@ { "released": "2026-07-19", "version": "2.6.0", - "notes": "Radar overhaul. (1) Real map tiles: the radar now draws over an OpenStreetMap basemap (self-hosted tile server supported via radar_tile_server, public mirrors as fallback; carto/carto_dark/esri styles too), with the classic WeatherStar vector map kept as a selectable style and as automatic fallback when tiles are unavailable — radar is no longer blank outside the US. (2) Accuracy fix: basemap and radar are now rendered through one shared Web-Mercator viewport and the radar is a mosaic of every tile in view, so precipitation finally lines up with the map on all panel sizes (previously the radar was composited at a different zoom than the map — up to 4x off on 64x32 — and cut off near tile edges). (3) Fresher data: optional RainViewer nowcast frames (~30 min of predicted radar, labeled FCST +Nm with yellow progress dots), index polled every 3 min by default with tiles only downloaded for new frames, and frames/tiles cached to disk so restarts rebuild the animation without refetching. (4) Easier config: new radar_range_miles (distance to panel edge) replaces the abstract radar_zoom (still honored for existing configs), plus map style/brightness, frame timing, and past-frame-count settings. (5) Smoother playback: time-based frame stepping with a hold on the newest frame, and optional dynamic duration to let the rotation wait for a full loop.", + "notes": "Radar overhaul. (1) Real map tiles: the radar now draws over an OpenStreetMap basemap (self-hosted tile server supported via radar_tile_server, public mirrors as fallback; carto/carto_dark/esri styles too), with the classic WeatherStar vector map kept as a selectable style and as automatic fallback when tiles are unavailable \u2014 radar is no longer blank outside the US. (2) Accuracy fix: basemap and radar are now rendered through one shared Web-Mercator viewport and the radar is a mosaic of every tile in view, so precipitation finally lines up with the map on all panel sizes (previously the radar was composited at a different zoom than the map \u2014 up to 4x off on 64x32 \u2014 and cut off near tile edges). (3) Fresher data: optional RainViewer nowcast frames (~30 min of predicted radar, labeled FCST +Nm with yellow progress dots), index polled every 3 min by default with tiles only downloaded for new frames, and frames/tiles cached to disk so restarts rebuild the animation without refetching. (4) Easier config: new radar_range_miles (distance to panel edge) replaces the abstract radar_zoom (still honored for existing configs), plus map style/brightness, frame timing, and past-frame-count settings. (5) Smoother playback: time-based frame stepping with a hold on the newest frame, and optional dynamic duration to let the rotation wait for a full loop.", "ledmatrix_min": "2.0.0" }, { @@ -65,7 +71,7 @@ { "released": "2026-06-10", "version": "2.5.0", - "note": "Geocode the configured location once and cache the coordinates permanently across update cycles and restarts, instead of re-resolving every refresh. Cities don't move, so the geocoding API is now only ever called on a cache miss — eliminating the per-refresh geocoding timeouts that previously aborted the whole weather update, blanked the widget, and triggered up-to-an-hour error backoff. Add optional location_latitude/location_longitude config fields to skip geocoding entirely.", + "note": "Geocode the configured location once and cache the coordinates permanently across update cycles and restarts, instead of re-resolving every refresh. Cities don't move, so the geocoding API is now only ever called on a cache miss \u2014 eliminating the per-refresh geocoding timeouts that previously aborted the whole weather update, blanked the widget, and triggered up-to-an-hour error backoff. Add optional location_latitude/location_longitude config fields to skip geocoding entirely.", "ledmatrix_min": "2.0.0" }, { @@ -144,7 +150,7 @@ "ledmatrix_min_version": "2.0.0" } ], - "last_updated": "2026-07-19", + "last_updated": "2026-08-05", "stars": 0, "downloads": 0, "verified": true, From 8ed9e2091f5ac9f4198fd0b9ebd75b8cca4a03f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 18:29:49 -0400 Subject: [PATCH 2/6] fix(weather): make the compact tile faithful to the screen it replaces The first cut of the Vegas tile was not the weather screen, just smaller. Two things were lost, both of which carry meaning: - Every optional metric. It rendered a fixed "UV/H/W" string, so feels-like, dew point, visibility and pressure vanished from the ticker regardless of the show_* toggles that are supposed to control them. - The per-item colors. Metrics were flattened into one dim string, which drops the UV severity grading -- the one colour on that row that means something. The tile now uses the same three rows in the same order as the full-screen layout (condition, temperature with high/low, metrics), and the metric list is built by a helper both renderers share, so the toggles and colours cannot drift apart again. That is the same failure the odds-ticker fix just dealt with: a second table that had to be kept in step by hand, and wasn't. Being faithful costs width, and the honest numbers are lower than the first pass claimed: 308px against 512px with dew point and visibility off (this device's config), 408px with every metric enabled -- not the 250px of the version that was quietly dropping them. The full-screen renderer now calls the shared helper too; its output is pixel-identical to before the refactor, verified by image diff. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 --- plugins/ledmatrix-weather/manager.py | 131 ++++++++++++++---------- plugins/ledmatrix-weather/manifest.json | 2 +- 2 files changed, 79 insertions(+), 54 deletions(-) diff --git a/plugins/ledmatrix-weather/manager.py b/plugins/ledmatrix-weather/manager.py index 51b8f447..89e38975 100644 --- a/plugins/ledmatrix-weather/manager.py +++ b/plugins/ledmatrix-weather/manager.py @@ -1103,35 +1103,9 @@ def _render_current_weather_image(self) -> Optional[Image.Image]: wind_dir = self._get_wind_direction(wind_deg) wind_gust = self.weather_data['wind'].get('gust') - # Core items (always shown) plus a droppable tag for width-aware - # thinning below — None means "never drop" (UV/H/W and, once - # enabled, feels-like are always kept; only dew point/visibility/ - # pressure are shed as the panel gets too narrow to fit them all - # legibly, least-useful first). - uv_color = self._get_uv_color(uv_index) - all_items = [] # list of (text, color, drop_tag) - all_items.append((f"UV:{uv_index:.0f}", uv_color, None)) - all_items.append((f"H:{humidity}%", self.COLORS['dim'], None)) - if wind_gust and wind_gust > wind_speed * 1.3: - all_items.append((f"W:{wind_speed:.0f}g{wind_gust:.0f}{wind_dir}", self.COLORS['dim'], None)) - else: - all_items.append((f"W:{wind_speed:.0f}{wind_dir}", self.COLORS['dim'], None)) - - # Extra items — merged into same row (no degree symbol, font can't render it) - if self.show_feels_like and feels_like is not None: - all_items.append((f"FL:{int(feels_like)}", self.COLORS['dim'], None)) - if self.show_dew_point and dew_point is not None: - all_items.append((f"Dew:{int(dew_point)}", self.COLORS['dim'], 'dew_point')) - if self.show_visibility and visibility_m is not None: - vis_val = visibility_m / 1609.34 if self.units == 'imperial' else visibility_m / 1000 - vis_u = "mi" if self.units == 'imperial' else "km" - all_items.append((f"Vis:{vis_val:.0f}{vis_u}", self.COLORS['dim'], 'visibility')) - if self.show_pressure and pressure is not None: - if self.units == 'imperial': - pv = pressure * 0.02953 - all_items.append((f"P:{pv:.2f}\"", self.COLORS['dim'], 'pressure')) - else: - all_items.append((f"P:{int(pressure)}hPa", self.COLORS['dim'], 'pressure')) + all_items = self._build_metric_items( + uv_index, humidity, wind_speed, wind_dir, wind_gust, + feels_like, dew_point, visibility_m, pressure) # Drop least-useful optional items, in this order, while an equal # split still leaves each item too cramped to read (below @@ -1157,6 +1131,46 @@ def _render_current_weather_image(self) -> Optional[Image.Image]: self.logger.exception("Error rendering current weather") return None + def _build_metric_items(self, uv_index, humidity, wind_speed, wind_dir, + wind_gust, feels_like, dew_point, visibility_m, + pressure) -> List[tuple]: + """The bottom-bar metrics as (text, color, drop_tag) tuples. + + Shared by the full-screen layout and the Vegas tile so the two cannot + drift: the colors carry meaning (UV is graded by severity) and the + show_* toggles decide what appears at all, neither of which should + depend on which renderer you happen to be looking at. + + drop_tag None means "never drop" — UV/H/W and, once enabled, + feels-like are always kept; only dew point/visibility/pressure are + shed when the panel is too narrow to fit them all legibly. + """ + items = [ + (f"UV:{uv_index:.0f}", self._get_uv_color(uv_index), None), + (f"H:{humidity}%", self.COLORS['dim'], None), + ] + if wind_gust and wind_gust > wind_speed * 1.3: + items.append((f"W:{wind_speed:.0f}g{wind_gust:.0f}{wind_dir}", + self.COLORS['dim'], None)) + else: + items.append((f"W:{wind_speed:.0f}{wind_dir}", self.COLORS['dim'], None)) + + # No degree symbol below — the bottom-bar font can't render it. + if self.show_feels_like and feels_like is not None: + items.append((f"FL:{int(feels_like)}", self.COLORS['dim'], None)) + if self.show_dew_point and dew_point is not None: + items.append((f"Dew:{int(dew_point)}", self.COLORS['dim'], 'dew_point')) + if self.show_visibility and visibility_m is not None: + vis_val = visibility_m / 1609.34 if self.units == 'imperial' else visibility_m / 1000 + vis_u = "mi" if self.units == 'imperial' else "km" + items.append((f"Vis:{vis_val:.0f}{vis_u}", self.COLORS['dim'], 'visibility')) + if self.show_pressure and pressure is not None: + if self.units == 'imperial': + items.append((f"P:{pressure * 0.02953:.2f}\"", self.COLORS['dim'], 'pressure')) + else: + items.append((f"P:{int(pressure)}hPa", self.COLORS['dim'], 'pressure')) + return items + def _display_current_weather(self) -> None: """Display current weather conditions using comprehensive layout with icons.""" try: @@ -1843,33 +1857,38 @@ def _render_current_weather_vegas_image(self) -> Optional[Image.Image]: temp_high = int(self.weather_data['main']['temp_max']) temp_low = int(self.weather_data['main']['temp_min']) - temp_font = self.display_manager.small_font small_font = self.display_manager.small_font tiny_font = self.display_manager.extra_small_font gap = max(3, round(4 * (height / 32.0))) + item_gap = max(3, round(4 * (height / 32.0))) icon_size = layout['current_icon_size'] - # Column 2 stacks temperature over high/low; column 3 stacks the - # condition over the metrics. Width is the widest line in each. + # Same three rows as the full-screen layout, in the same order, so + # the tile reads as the familiar screen rather than a new one: + # condition, then temperature with its high/low, then the metrics. temp_text = f"{temp}°" high_low_text = f"{temp_low}°/{temp_high}°" - metrics_text = (f"UV:{uv_index:.0f} H:{humidity}% " - f"W:{wind_speed:.0f}{self._get_wind_direction(wind_deg)}") + metric_items = self._build_metric_items( + uv_index, humidity, wind_speed, self._get_wind_direction(wind_deg), + self.weather_data['wind'].get('gust'), + self.weather_data['main'].get('feels_like'), + self.weather_data['main'].get('dew_point'), + self.weather_data['main'].get('visibility'), + self.weather_data['main'].get('pressure')) measure = ImageDraw.Draw(Image.new('RGB', (1, 1))) def text_w(text, font): return int(measure.textlength(text, font=font)) - temp_w = text_w(temp_text, temp_font) - high_low_w = text_w(high_low_text, small_font) + temp_row_w = text_w(temp_text, small_font) + gap + text_w(high_low_text, small_font) condition_w = text_w(condition, small_font) - metrics_w = text_w(metrics_text, tiny_font) + metrics_w = sum(text_w(t, tiny_font) for t, _c, _d in metric_items) + metrics_w += item_gap * max(0, len(metric_items) - 1) - col2_w = max(temp_w, high_low_w) - col3_w = max(condition_w, metrics_w) - total_width = icon_size + gap + col2_w + gap + col3_w + gap + text_col_w = max(temp_row_w, condition_w, metrics_w) + total_width = icon_size + gap + text_col_w + gap # On a narrow panel this side-by-side arrangement is *wider* than # the full-screen tile it replaces (198px against 128px at @@ -1888,22 +1907,28 @@ def text_w(text, font): WeatherIcons.draw_weather_icon( img, icon_code, 0, max(0, (height - icon_size) // 2), size=icon_size) - # Two text rows, vertically centred as a pair. + # Three rows centred as a block, spaced to whatever height allows. + rows = 3 row_h = 8 - top_y = max(0, (height - (row_h * 2 + 2)) // 2) - bottom_y = top_y + row_h + 2 - + spacing = max(1, (height - rows * row_h) // (rows + 1)) x = icon_size + gap - draw.text((x, top_y), temp_text, font=temp_font, + y = spacing + + draw.text((x, y), condition, font=small_font, fill=self.COLORS['text']) + + y += row_h + spacing + draw.text((x, y), temp_text, font=small_font, fill=self.COLORS['highlight']) - draw.text((x, bottom_y), high_low_text, font=small_font, - fill=self.COLORS['dim']) - - x += col2_w + gap - draw.text((x, top_y), condition, font=small_font, - fill=self.COLORS['text']) - draw.text((x, bottom_y), metrics_text, font=tiny_font, - fill=self.COLORS['dim']) + draw.text((x + text_w(temp_text, small_font) + gap, y), high_low_text, + font=small_font, fill=self.COLORS['dim']) + + # Metrics keep their own colors — UV is graded by severity, so + # flattening them to one dim string would drop the meaning. + y += row_h + spacing + mx = x + for text, color, _drop_tag in metric_items: + draw.text((mx, y), text, font=tiny_font, fill=color) + mx += text_w(text, tiny_font) + item_gap return img diff --git a/plugins/ledmatrix-weather/manifest.json b/plugins/ledmatrix-weather/manifest.json index 4c036f6e..d950af86 100644 --- a/plugins/ledmatrix-weather/manifest.json +++ b/plugins/ledmatrix-weather/manifest.json @@ -29,7 +29,7 @@ { "version": "2.6.3", "released": "2026-08-05", - "notes": "Give the Vegas ticker a content-sized current-conditions tile. The full-screen renderer anchors condition, temperature and high/low to the right edge of the panel and spreads the metrics bar across the whole width, so reused as a ticker tile it costs a full display width -- three weather modes came to 1536px, of which auto_trim could reclaim only 14% because the ink genuinely reaches both edges, while other plugins hand the ticker tiles of 110-145px. The ticker now gets its own left-to-right layout measured to exactly what it draws: 250px instead of 512px on a 512-wide panel. On narrow panels, where fixed-size bitmap fonts make the side-by-side arrangement wider than the screen, it falls back to the full-screen tile rather than costing more than it saves. The normal rotation slot is unchanged.", + "notes": "Give the Vegas ticker a content-sized current-conditions tile. The full-screen renderer anchors condition, temperature and high/low to the right edge of the panel and spreads the metrics bar across the whole width, so reused as a ticker tile it costs a full display width -- three weather modes came to 1536px, of which auto_trim reclaimed only 14% because the ink reaches both edges even though the middle is empty. The ticker now gets its own layout with the same three rows in the same order, sized to what it draws: 308px instead of 512px with dew point and visibility off, 408px with every metric enabled. Metrics keep their individual colors, so UV stays graded by severity, and the show_* toggles still decide what appears -- both renderers now build that list from one shared helper rather than duplicating it. On panels where fixed-size bitmap fonts make the packed layout no narrower than the screen it falls back to the full-screen tile, so narrow displays reclaim nothing rather than paying more. The full-screen render is pixel-identical to before.", "ledmatrix_min_version": "2.0.0" }, { From 3cf543122d6d65bda8a8ad84c4699138d72c70dd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 18:46:35 -0400 Subject: [PATCH 3/6] fix(weather): pack the hourly and daily Vegas tiles too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I scoped these out of the first pass with the claim that they are "genuinely multi-column and fill their width". Rendering them showed the opposite, and they are the worse offenders: current conditions 49% inked ~49px gaps 43% reclaimable hourly 28% inked 91px gaps 67% reclaimable daily 20% inked 136px gaps 77% reclaimable Daily is 80% black. Both renderers lay their columns out as `width // count`, so they inherit whatever width they are handed: three days across a 512px panel puts 136px of nothing between each column. Both now take an optional width, defaulting to the panel as before, and the Vegas path passes one sized to the columns: 512px -> 272px for hourly, 512px -> 204px for daily. Together with the current-conditions tile the three come to 784px instead of 1536px on this device. Same guard as before — `_compact_forecast_width()` returns None when packing would not actually be narrower, and the caller falls back to the panel-width render. At 256x64 hourly already fits, so only daily packs; at 128x32 hourly stays as-is and daily gains 11%. Normal rotation is untouched: all five modes render pixel-identical at 128x32, 256x64 and 512x64, verified by image diff. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 --- plugins/ledmatrix-weather/manager.py | 53 +++++++++++++++++++++---- plugins/ledmatrix-weather/manifest.json | 2 +- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/plugins/ledmatrix-weather/manager.py b/plugins/ledmatrix-weather/manager.py index 89e38975..774b6a7e 100644 --- a/plugins/ledmatrix-weather/manager.py +++ b/plugins/ledmatrix-weather/manager.py @@ -1241,13 +1241,18 @@ def _get_daily_state(self) -> List[Dict[str, Any]]: for f in self.daily_forecast[:4] ] - def _render_hourly_forecast_image(self) -> Optional[Image.Image]: - """Render hourly forecast to an Image without display side effects.""" + def _render_hourly_forecast_image(self, width: Optional[int] = None) -> Optional[Image.Image]: + """Render hourly forecast to an Image without display side effects. + + width defaults to the panel. The Vegas ticker passes a narrower one: + columns are laid out as `width // count`, so on a wide panel four + hours get ~128px each to hold ~36px of content. + """ try: if not self.hourly_forecast: return None - width = self.display_manager.matrix.width + width = width or self.display_manager.matrix.width height = self.display_manager.matrix.height img = Image.new('RGB', (width, height), (0, 0, 0)) draw = ImageDraw.Draw(img) @@ -1313,13 +1318,18 @@ def _display_hourly_forecast(self) -> None: except Exception as e: self.logger.error(f"Error displaying hourly forecast: {e}") - def _render_daily_forecast_image(self) -> Optional[Image.Image]: - """Render daily forecast to an Image without display side effects.""" + def _render_daily_forecast_image(self, width: Optional[int] = None) -> Optional[Image.Image]: + """Render daily forecast to an Image without display side effects. + + width defaults to the panel. The Vegas ticker passes a narrower one: + three days spread across a 512px panel leaves ~136px of black between + each column. + """ try: if not self.daily_forecast: return None - width = self.display_manager.matrix.width + width = width or self.display_manager.matrix.width height = self.display_manager.matrix.height img = Image.new('RGB', (width, height), (0, 0, 0)) draw = ImageDraw.Draw(img) @@ -1828,6 +1838,31 @@ def reset_cycle_state(self) -> None: if fetcher is not None: fetcher.reset_loop() + def _compact_forecast_width(self, columns: int) -> Optional[int]: + """Width a forecast needs for `columns`, rather than the whole panel. + + The forecast renderers lay out columns as `width // count`, so they + inherit whatever width they are given: on a 512px panel three daily + columns are ~136px of black apart, and the tile measures 20% inked. + Sizing to the content is what stops the ticker scrolling through that. + + Returns None to mean "use the panel width" when packing would not + actually be narrower -- a small display is already tight. + """ + if columns <= 0: + return None + + layout = self._get_layout() + # A column holds an icon above a short label and temperature; the icon + # is the wider of the two in every size we render at. + column_w = max(layout['forecast_icon_size'], 28) + 8 + compact = column_w * columns + + panel_w = self.display_manager.matrix.width + if compact >= panel_w: + return None + return compact + def _render_current_weather_vegas_image(self) -> Optional[Image.Image]: """Current conditions sized to its content, for the Vegas ticker. @@ -1951,12 +1986,14 @@ def get_vegas_content(self): images.append(img) if self.show_hourly and self.hourly_forecast: - img = self._render_hourly_forecast_image() + img = self._render_hourly_forecast_image( + width=self._compact_forecast_width(min(4, len(self.hourly_forecast)))) if img: images.append(img) if self.show_daily and self.daily_forecast: - img = self._render_daily_forecast_image() + img = self._render_daily_forecast_image( + width=self._compact_forecast_width(min(3, len(self.daily_forecast)))) if img: images.append(img) diff --git a/plugins/ledmatrix-weather/manifest.json b/plugins/ledmatrix-weather/manifest.json index d950af86..8930e0fc 100644 --- a/plugins/ledmatrix-weather/manifest.json +++ b/plugins/ledmatrix-weather/manifest.json @@ -29,7 +29,7 @@ { "version": "2.6.3", "released": "2026-08-05", - "notes": "Give the Vegas ticker a content-sized current-conditions tile. The full-screen renderer anchors condition, temperature and high/low to the right edge of the panel and spreads the metrics bar across the whole width, so reused as a ticker tile it costs a full display width -- three weather modes came to 1536px, of which auto_trim reclaimed only 14% because the ink reaches both edges even though the middle is empty. The ticker now gets its own layout with the same three rows in the same order, sized to what it draws: 308px instead of 512px with dew point and visibility off, 408px with every metric enabled. Metrics keep their individual colors, so UV stays graded by severity, and the show_* toggles still decide what appears -- both renderers now build that list from one shared helper rather than duplicating it. On panels where fixed-size bitmap fonts make the packed layout no narrower than the screen it falls back to the full-screen tile, so narrow displays reclaim nothing rather than paying more. The full-screen render is pixel-identical to before.", + "notes": "Size all three Vegas weather tiles to their content instead of the panel. Each mode was rendered at the full display width, so the ticker scrolled through three display widths of mostly black -- measured at 49% inked for current conditions, 28% for hourly and 20% for daily, with 91px and 136px of dead space between forecast columns. auto_trim reclaimed only 14% because the ink reaches both edges even where the middle is empty. On a 512px panel the three tiles now come to 308+272+204px instead of 1536px. Current conditions keeps the same three rows in the same order, metrics keep their individual colors so UV stays graded by severity, and the show_* toggles still decide what appears; the forecasts keep every column, just packed. Where packing would not be narrower -- small panels, where fixed-size bitmap fonts do not shrink -- each tile falls back to the panel-width render. Normal rotation is pixel-identical for all five modes at 128x32, 256x64 and 512x64.", "ledmatrix_min_version": "2.0.0" }, { From e36dd79da4ca776df4c633791fefaa9a9bee584a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 19:13:11 -0400 Subject: [PATCH 4/6] refactor(weather): make the compact current tile the real layout, not a copy The Vegas tile was a second layout that resembled the weather screen. Reviewing it against the original showed why that is the wrong shape: it had already lost the optional metrics and the UV colour once, and a parallel layout is something someone has to keep in step by hand. `_render_current_weather_image()` now takes an optional width, like the two forecast renderers already do, and the Vegas path simply calls it with a measured one. The condition/temperature/high-low stack right-aligns to whatever width it is given and the metrics bar divides that width, so a narrower canvas is the entire change: same icon, same three-line stack, same bottom bar, drawn closer together. Deletes ~90 lines of duplicated layout, and comes out narrower than the hand-built version because the real layout packs better: 270px against 308px with this device's metric config, 378px against 408px with every metric on. Normal rotation stays pixel-identical for all five modes at 128x32, 256x64 and 512x64, verified by image diff. Pre-existing and untouched: the amber temperature slightly overlaps the tail of a long condition string. It does that at full width too, so fixing it would change the normal rotation render, which this PR deliberately leaves alone. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 --- plugins/ledmatrix-weather/manager.py | 168 +++++++++--------------- plugins/ledmatrix-weather/manifest.json | 2 +- 2 files changed, 64 insertions(+), 106 deletions(-) diff --git a/plugins/ledmatrix-weather/manager.py b/plugins/ledmatrix-weather/manager.py index 774b6a7e..7d0af650 100644 --- a/plugins/ledmatrix-weather/manager.py +++ b/plugins/ledmatrix-weather/manager.py @@ -1039,10 +1039,16 @@ def _display_no_data(self) -> None: self.display_manager.image = img self.display_manager.update_display() - def _render_current_weather_image(self) -> Optional[Image.Image]: - """Render current weather conditions to an Image without display side effects.""" + def _render_current_weather_image(self, width: Optional[int] = None) -> Optional[Image.Image]: + """Render current weather conditions to an Image without display side effects. + + width defaults to the panel. The Vegas ticker passes a narrower one so + the tile is this same layout simply drawn closer together: the + condition/temperature/high-low stack right-aligns to whatever width it + is given, and the metrics bar divides that width rather than the panel. + """ try: - width = self.display_manager.matrix.width + width = width or self.display_manager.matrix.width height = self.display_manager.matrix.height img = Image.new('RGB', (width, height), (0, 0, 0)) draw = ImageDraw.Draw(img) @@ -1863,115 +1869,67 @@ def _compact_forecast_width(self, columns: int) -> Optional[int]: return None return compact - def _render_current_weather_vegas_image(self) -> Optional[Image.Image]: - """Current conditions sized to its content, for the Vegas ticker. - - The full-screen renderer anchors the condition, temperature and - high/low to the *right edge of the panel* and spreads the metrics bar - evenly across the whole width. That is right for a screen you look at, - but as a ticker tile it means one display width per weather mode -- - three of them, 1536px, of which auto_trim could only reclaim 14% - because the ink genuinely reaches both edges. Other plugins hand the - ticker tiles of 110-145px. - - So the ticker gets its own left-to-right layout, measured and sized to - exactly what it draws. The full-screen version is untouched and still - serves the normal rotation slot. + def _compact_current_width(self) -> Optional[int]: + """Width the current-conditions layout needs, rather than the panel. + + Measures the two things that set it: the condition/temperature/high-low + stack that right-aligns after the icon, and the metrics bar, which + divides the width into equal sections and so needs room for its widest + item in every one of them. + + Returns None for "use the panel width" when packing would not be + narrower. """ try: - height = self.display_manager.matrix.height layout = self._get_layout() - - temp = int(self.weather_data['main']['temp']) - condition = self.weather_data['weather'][0]['main'] - icon_code = self.weather_data['weather'][0]['icon'] - humidity = self.weather_data['main']['humidity'] - wind_speed = self.weather_data['wind'].get('speed', 0) - wind_deg = self.weather_data['wind'].get('deg', 0) - uv_index = self.weather_data['main'].get('uvi', 0) - temp_high = int(self.weather_data['main']['temp_max']) - temp_low = int(self.weather_data['main']['temp_min']) - - small_font = self.display_manager.small_font - tiny_font = self.display_manager.extra_small_font - - gap = max(3, round(4 * (height / 32.0))) - item_gap = max(3, round(4 * (height / 32.0))) - icon_size = layout['current_icon_size'] - - # Same three rows as the full-screen layout, in the same order, so - # the tile reads as the familiar screen rather than a new one: - # condition, then temperature with its high/low, then the metrics. - temp_text = f"{temp}°" - high_low_text = f"{temp_low}°/{temp_high}°" - metric_items = self._build_metric_items( - uv_index, humidity, wind_speed, self._get_wind_direction(wind_deg), - self.weather_data['wind'].get('gust'), - self.weather_data['main'].get('feels_like'), - self.weather_data['main'].get('dew_point'), - self.weather_data['main'].get('visibility'), - self.weather_data['main'].get('pressure')) - + small = self.display_manager.small_font + tiny = self.display_manager.extra_small_font measure = ImageDraw.Draw(Image.new('RGB', (1, 1))) - def text_w(text, font): - return int(measure.textlength(text, font=font)) - - temp_row_w = text_w(temp_text, small_font) + gap + text_w(high_low_text, small_font) - condition_w = text_w(condition, small_font) - metrics_w = sum(text_w(t, tiny_font) for t, _c, _d in metric_items) - metrics_w += item_gap * max(0, len(metric_items) - 1) - - text_col_w = max(temp_row_w, condition_w, metrics_w) - total_width = icon_size + gap + text_col_w + gap - - # On a narrow panel this side-by-side arrangement is *wider* than - # the full-screen tile it replaces (198px against 128px at - # 128x32), because fixed-size bitmap fonts do not shrink with the - # display. Reclaiming nothing is fine; costing extra is not. - if total_width >= self.display_manager.matrix.width: - self.logger.debug( - "[Weather Vegas] Compact current tile would be %dpx on a " - "%dpx panel; using the full-screen layout instead", - total_width, self.display_manager.matrix.width) - return self._render_current_weather_image() - - img = Image.new('RGB', (total_width, height), (0, 0, 0)) - draw = ImageDraw.Draw(img) - - WeatherIcons.draw_weather_icon( - img, icon_code, 0, max(0, (height - icon_size) // 2), size=icon_size) - - # Three rows centred as a block, spaced to whatever height allows. - rows = 3 - row_h = 8 - spacing = max(1, (height - rows * row_h) // (rows + 1)) - x = icon_size + gap - y = spacing - - draw.text((x, y), condition, font=small_font, fill=self.COLORS['text']) - - y += row_h + spacing - draw.text((x, y), temp_text, font=small_font, - fill=self.COLORS['highlight']) - draw.text((x + text_w(temp_text, small_font) + gap, y), high_low_text, - font=small_font, fill=self.COLORS['dim']) - - # Metrics keep their own colors — UV is graded by severity, so - # flattening them to one dim string would drop the meaning. - y += row_h + spacing - mx = x - for text, color, _drop_tag in metric_items: - draw.text((mx, y), text, font=tiny_font, fill=color) - mx += text_w(text, tiny_font) + item_gap + main = self.weather_data['main'] + temp = int(main['temp']) + condition = self.weather_data['weather'][0]['main'] + stack_w = max( + measure.textlength(condition, font=small), + measure.textlength(f"{temp}\u00b0", font=small), + measure.textlength( + f"{int(main['temp_min'])}\u00b0/{int(main['temp_max'])}\u00b0", + font=small), + ) + gap = max(4, layout['right_margin'] * 2) + text_block = (layout['current_icon_x'] + layout['current_icon_size'] + + gap + int(stack_w) + layout['right_margin']) + + items = self._build_metric_items( + main.get('uvi', 0), main['humidity'], + self.weather_data['wind'].get('speed', 0), + self._get_wind_direction(self.weather_data['wind'].get('deg', 0)), + self.weather_data['wind'].get('gust'), main.get('feels_like'), + main.get('dew_point'), main.get('visibility'), main.get('pressure')) + metrics_block = 0 + if items: + widest = max(measure.textlength(t, font=tiny) for t, _c, _d in items) + metrics_block = (int(widest) + 6) * len(items) + + compact = max(text_block, metrics_block) + panel_w = self.display_manager.matrix.width + if compact >= panel_w: + return None + return compact + except Exception as e: + self.logger.debug("Could not size the compact current tile: %s", e) + return None - return img + def _render_current_weather_vegas_image(self) -> Optional[Image.Image]: + """Current conditions for the Vegas ticker: the same layout, packed. - except Exception as e: - self.logger.error("Error rendering compact current weather: %s", e, - exc_info=True) - # Fall back to the full-screen tile rather than dropping the mode. - return self._render_current_weather_image() + Reusing the real renderer at a narrower width keeps the tile the + familiar screen -- icon left, condition over temperature over + high/low, metrics along the bottom -- instead of a second layout that + merely resembles it and drifts. Everything right-aligns to the width it + is handed, so the only change is how much black sits between the parts. + """ + return self._render_current_weather_image(width=self._compact_current_width()) def get_vegas_content(self): """Return images for all enabled weather display modes.""" diff --git a/plugins/ledmatrix-weather/manifest.json b/plugins/ledmatrix-weather/manifest.json index 8930e0fc..41104cac 100644 --- a/plugins/ledmatrix-weather/manifest.json +++ b/plugins/ledmatrix-weather/manifest.json @@ -29,7 +29,7 @@ { "version": "2.6.3", "released": "2026-08-05", - "notes": "Size all three Vegas weather tiles to their content instead of the panel. Each mode was rendered at the full display width, so the ticker scrolled through three display widths of mostly black -- measured at 49% inked for current conditions, 28% for hourly and 20% for daily, with 91px and 136px of dead space between forecast columns. auto_trim reclaimed only 14% because the ink reaches both edges even where the middle is empty. On a 512px panel the three tiles now come to 308+272+204px instead of 1536px. Current conditions keeps the same three rows in the same order, metrics keep their individual colors so UV stays graded by severity, and the show_* toggles still decide what appears; the forecasts keep every column, just packed. Where packing would not be narrower -- small panels, where fixed-size bitmap fonts do not shrink -- each tile falls back to the panel-width render. Normal rotation is pixel-identical for all five modes at 128x32, 256x64 and 512x64.", + "notes": "Size all three Vegas weather tiles to their content instead of the panel. Each mode was rendered at the full display width, so the ticker scrolled through three display widths of mostly black -- measured at 49% inked for current conditions, 28% for hourly and 20% for daily, with 91px and 136px of dead space between forecast columns, and auto_trim could reclaim only 14% because the ink reaches both edges even where the middle is empty. All three renderers now take an optional width, defaulting to the panel exactly as before, and the Vegas path passes one measured from the content: on a 512px panel the tiles come to 270+272+204px instead of 1536px. The layouts themselves are unchanged -- the current tile is still icon, condition over temperature over high/low, and the metrics bar along the bottom, just drawn closer together -- so nothing needs a second layout kept in step by hand. Metric items are now built by one helper shared with the full-screen path, keeping the show_* toggles and the UV severity colour in agreement. Where packing would not be narrower, each tile falls back to the panel-width render. Normal rotation is pixel-identical for all five modes at 128x32, 256x64 and 512x64.", "ledmatrix_min_version": "2.0.0" }, { From b8196274e35a4a73322f7eca892c540d7bd786b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 19:20:39 -0400 Subject: [PATCH 5/6] fix(weather): keep the compact metrics bar readable Reported as the bottom stats being harder to read. The font is unchanged -- extra_small_font, PressStart2P at 6px, in both paths -- but the spacing was not. The metrics bar centres each item in an equal section of whatever width it is given, so narrowing the tile squeezes the items together. Sizing the tile to the text plus 6px left a 15px minimum gap against the 63px of the full-width bar: four times tighter, which is fine in a still image and tiring to read scrolling past. The compact width now reserves a real gap per item instead. 24px, as a readability floor rather than a packing minimum -- MIN_METRIC_ITEM_WIDTH_PX is where items start to collide, which is a long way below where a moving row is comfortable. Current conditions goes 270px -> 360px, minimum gap 15px -> 33px. Still 30% narrower than the panel on this device's metric config. With every metric enabled seven items cannot be spaced legibly under ~500px, so the tile is 504px and reclaims almost nothing -- the honest answer at that density rather than a tighter unreadable one. Normal rotation stays pixel-identical for all five modes at three sizes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 --- plugins/ledmatrix-weather/manager.py | 13 ++++++++++++- plugins/ledmatrix-weather/manifest.json | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/plugins/ledmatrix-weather/manager.py b/plugins/ledmatrix-weather/manager.py index 7d0af650..3d0c032c 100644 --- a/plugins/ledmatrix-weather/manager.py +++ b/plugins/ledmatrix-weather/manager.py @@ -153,6 +153,11 @@ def __init__(self, plugin_id: str, config: Dict[str, Any], # reference fitting all 7 possible items (192 // 7 ~= 27px) without # crowding. self.MIN_METRIC_ITEM_WIDTH_PX = 27 + # Gap the compact Vegas tile keeps between metrics-bar items. This is a + # readability floor, not a packing minimum: MIN_METRIC_ITEM_WIDTH_PX + # above is the point where items start to collide, which is a long way + # below the point where a scrolling row is comfortable to read. + self.COMPACT_METRIC_GAP_PX = 24 self.COLORS = { 'text': (255, 255, 255), 'highlight': (255, 200, 0), @@ -1909,7 +1914,13 @@ def _compact_current_width(self) -> Optional[int]: metrics_block = 0 if items: widest = max(measure.textlength(t, font=tiny) for t, _c, _d in items) - metrics_block = (int(widest) + 6) * len(items) + # The bar centres every item in an equal section, so the gap + # left between neighbours is (section - widest). Sizing to the + # text plus a few pixels squeezed that to 15px against the + # 63px of the full-width bar, which is legible in a still + # image and tiring to read scrolling past. Reserve a real gap + # instead and give back the width it costs. + metrics_block = (int(widest) + self.COMPACT_METRIC_GAP_PX) * len(items) compact = max(text_block, metrics_block) panel_w = self.display_manager.matrix.width diff --git a/plugins/ledmatrix-weather/manifest.json b/plugins/ledmatrix-weather/manifest.json index 41104cac..61636ba7 100644 --- a/plugins/ledmatrix-weather/manifest.json +++ b/plugins/ledmatrix-weather/manifest.json @@ -29,7 +29,7 @@ { "version": "2.6.3", "released": "2026-08-05", - "notes": "Size all three Vegas weather tiles to their content instead of the panel. Each mode was rendered at the full display width, so the ticker scrolled through three display widths of mostly black -- measured at 49% inked for current conditions, 28% for hourly and 20% for daily, with 91px and 136px of dead space between forecast columns, and auto_trim could reclaim only 14% because the ink reaches both edges even where the middle is empty. All three renderers now take an optional width, defaulting to the panel exactly as before, and the Vegas path passes one measured from the content: on a 512px panel the tiles come to 270+272+204px instead of 1536px. The layouts themselves are unchanged -- the current tile is still icon, condition over temperature over high/low, and the metrics bar along the bottom, just drawn closer together -- so nothing needs a second layout kept in step by hand. Metric items are now built by one helper shared with the full-screen path, keeping the show_* toggles and the UV severity colour in agreement. Where packing would not be narrower, each tile falls back to the panel-width render. Normal rotation is pixel-identical for all five modes at 128x32, 256x64 and 512x64.", + "notes": "Size the Vegas weather tiles to their content instead of the panel. Each mode rendered at full display width, so the ticker scrolled three display widths of mostly black -- 49% inked for current conditions, 28% hourly, 20% daily, with 91px and 136px of dead space between forecast columns, and auto_trim could reclaim only 14% because the ink reaches both edges even where the middle is empty. All three renderers now take an optional width, defaulting to the panel exactly as before, and the Vegas path passes one measured from the content; the layouts are unchanged, just drawn closer together. On a 512px panel the tiles come to 360+272+204px instead of 1536px. The metrics bar keeps a real gap between items rather than packing to the text: the bar centres each item in an equal section, so sizing to the text alone left 15px between them against 63px at full width, which is legible in a still image and tiring scrolling past. Metric items are built by one helper shared with the full-screen path, keeping the show_* toggles and the UV severity colour in agreement. Where packing would not be narrower each tile falls back to the panel-width render. Normal rotation is pixel-identical for all five modes at 128x32, 256x64 and 512x64.", "ledmatrix_min_version": "2.0.0" }, { From 1087a691df3b19af3b3c1782d15c56e6da32b486 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 19:28:06 -0400 Subject: [PATCH 6/6] docs(weather): correct the tile widths in the release note No code change. The figures in the previous note were measured with the wrong font: my offline harness stubbed extra_small_font as PressStart2P at 6px, but DisplayManager loads assets/fonts/4x6-font.ttf for it. That is roughly 40% narrower -- "W:11g15S" is 29px, not the 48px I measured -- so every metrics-bar number I reported was inflated, and the renders I produced showed the bottom row in the wrong typeface entirely. The code was always right: _compact_current_width() measures with self.display_manager.extra_small_font, so on a real device it has been computing the correct width throughout. Only the harness and the documentation were wrong. Corrected: current conditions is 265px, not 360px (48% reclaimed rather than 30%), and the three tiles come to 741px against 1536px. The gap the metrics bar had before the readability fix was ~6px against 79px at full width, not 15px against 63px. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5 --- plugins/ledmatrix-weather/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ledmatrix-weather/manifest.json b/plugins/ledmatrix-weather/manifest.json index 61636ba7..289ef259 100644 --- a/plugins/ledmatrix-weather/manifest.json +++ b/plugins/ledmatrix-weather/manifest.json @@ -29,7 +29,7 @@ { "version": "2.6.3", "released": "2026-08-05", - "notes": "Size the Vegas weather tiles to their content instead of the panel. Each mode rendered at full display width, so the ticker scrolled three display widths of mostly black -- 49% inked for current conditions, 28% hourly, 20% daily, with 91px and 136px of dead space between forecast columns, and auto_trim could reclaim only 14% because the ink reaches both edges even where the middle is empty. All three renderers now take an optional width, defaulting to the panel exactly as before, and the Vegas path passes one measured from the content; the layouts are unchanged, just drawn closer together. On a 512px panel the tiles come to 360+272+204px instead of 1536px. The metrics bar keeps a real gap between items rather than packing to the text: the bar centres each item in an equal section, so sizing to the text alone left 15px between them against 63px at full width, which is legible in a still image and tiring scrolling past. Metric items are built by one helper shared with the full-screen path, keeping the show_* toggles and the UV severity colour in agreement. Where packing would not be narrower each tile falls back to the panel-width render. Normal rotation is pixel-identical for all five modes at 128x32, 256x64 and 512x64.", + "notes": "Size the Vegas weather tiles to their content instead of the panel. Each mode rendered at full display width, so the ticker scrolled three display widths of mostly black -- 49% inked for current conditions, 28% hourly, 20% daily, with 91px and 136px of dead space between forecast columns, and auto_trim could reclaim only 14% because the ink reaches both edges even where the middle is empty. All three renderers now take an optional width, defaulting to the panel exactly as before, and the Vegas path passes one measured from the content; the layouts are unchanged, just drawn closer together. On a 512px panel the tiles come to 265+272+204px instead of 1536px. The metrics bar reserves a real gap between items rather than packing to the text, because the bar centres each item in an equal section: sizing to the text alone left about 6px between them against 79px at full width. Metric items are built by one helper shared with the full-screen path, keeping the show_* toggles and the UV severity colour in agreement. Where packing would not be narrower each tile falls back to the panel-width render. Normal rotation is pixel-identical for all five modes at 128x32, 256x64 and 512x64.", "ledmatrix_min_version": "2.0.0" }, {