""" {{ plugin_name }} — LEDMatrix Plugin Generated by LEDMatrix Plugin Composer on {{ generated_date }} Extension points: update() → add HTTP/MQTT data-fetching logic here _get_display_values() → map fetched data to display strings display() → add new elements or adapt layout per display size """ from src.plugin_system.base_plugin import BasePlugin {% if has_clock or has_countdown %} from datetime import datetime {% endif %} {% if has_blink %} import time {% endif %} {% if has_text_template %} from collections import defaultdict {% endif %} class {{ class_name }}(BasePlugin): def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager): super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager) {% for var in config_vars %} self.{{ var.key }} = config.get({{ var.key | tojson }}, {{ var.default | tojson }}) {% endfor %} # Live data cache — populated by update(); always {} in static layouts self._data = {} def update(self): """Fetch and refresh display data. For dynamic plugins: fetch from APIs/MQTT here and store in self._data. _get_display_values() will read self._data to produce display strings. """ # --- Data sources (add fetch logic here for dynamic plugins) --- pass def _get_display_values(self): """Map config variables and live data to display-ready strings. This is the single extension point for v2 data sources: add self._data lookups here once update() populates them. """ return { {% for var in config_vars %} {{ var.key | tojson }}: str(self.{{ var.key }}), {% endfor %} } def display(self, force_clear=False): try: {% if has_text_template %} values = defaultdict(str, self._get_display_values()) {% else %} values = self._get_display_values() {% endif %} if force_clear: self.display_manager.clear() width = self.display_manager.width height = self.display_manager.height {% if bg_color %} self.display_manager.draw.rectangle([0, 0, width, height], fill={{ bg_color }}) {% endif %} # ── Elements (rendered bottom to top) ────────────────────────── {% for el in elements %} {% set p = " " if el.min_width > 0 else " " %} {% set pi = (p + " ") if el.blink else p %} {% if el.min_width > 0 %} if width >= {{ el.min_width }}: # breakpoint: {{ el.min_width }}px+ displays only {% endif %} {% if el.blink %} {{ p }}if int(time.time() * 2) % 2: {% endif %} {% if el.type == 'text' %} {{ pi }}self.display_manager.draw_text( {% if el.text_is_template %} {{ pi }} {{ el.text | tojson }}.format_map(values), {% else %} {{ pi }} {{ el.text | tojson }}, {% endif %} {{ pi }} x={{ el.x_expr }}, y={{ el.y_expr }}, {{ pi }} color={{ el.rgb_tuple }}, {{ pi }} font=self.display_manager.{{ el.font_attr }}, {{ pi }}) {% if el.text2 %} {{ pi }}self.display_manager.draw_text( {% if el.text_is_template %} {{ pi }} {{ el.text2 | tojson }}.format_map(values), {% else %} {{ pi }} {{ el.text2 | tojson }}, {% endif %} {{ pi }} x={{ el.x2_expr }}, y={{ el.y2_expr }}, {{ pi }} color={{ el.rgb_tuple }}, {{ pi }} font=self.display_manager.{{ el.font_attr }}, {{ pi }}) {% endif %} {% elif el.type == 'dynamic_text' %} {% if el.binding_source == 'config' %} {{ pi }}self.display_manager.draw_text( {{ pi }} values.get({{ el.binding_key | tojson }}, ''), {{ pi }} x={{ el.x_expr }}, y={{ el.y_expr }}, {{ pi }} color={{ el.rgb_tuple }}, {{ pi }} font=self.display_manager.{{ el.font_attr }}, {{ pi }}) {% endif %} {% elif el.type == 'clock' %} {{ pi }}self.display_manager.draw_text( {{ pi }} datetime.now().strftime({{ el.format | tojson }}), {{ pi }} x={{ el.x_expr }}, y={{ el.y_expr }}, {{ pi }} color={{ el.rgb_tuple }}, {{ pi }} font=self.display_manager.{{ el.font_attr }}, {{ pi }}) {% if el.format2 %} {{ pi }}self.display_manager.draw_text( {{ pi }} datetime.now().strftime({{ el.format2 | tojson }}), {{ pi }} x={{ el.x2_expr }}, y={{ el.y2_expr }}, {{ pi }} color={{ el.rgb_tuple }}, {{ pi }} font=self.display_manager.{{ el.font_attr }}, {{ pi }}) {% endif %} {% elif el.type == 'countdown' %} {{ pi }}_cd_target = float(values.get({{ el.binding_key | tojson }}, 0) or 0) {{ pi }}_cd_secs = max(0.0, _cd_target - datetime.now().timestamp()) {% if el.countdown_format == 'dhms' %} {{ pi }}_cd_d, _cd_rem = divmod(int(_cd_secs), 86400) {{ pi }}_cd_h, _cd_rem = divmod(_cd_rem, 3600) {{ pi }}_cd_m, _cd_s = divmod(_cd_rem, 60) {{ pi }}_cd_str = f'{_cd_d}d {_cd_h:02d}:{_cd_m:02d}:{_cd_s:02d}' {% elif el.countdown_format == 'hms' %} {{ pi }}_cd_h, _cd_rem = divmod(int(_cd_secs), 3600) {{ pi }}_cd_m, _cd_s = divmod(_cd_rem, 60) {{ pi }}_cd_str = f'{_cd_h}h {_cd_m:02d}:{_cd_s:02d}' {% elif el.countdown_format == 'dhm' %} {{ pi }}_cd_d, _cd_rem = divmod(int(_cd_secs), 86400) {{ pi }}_cd_h, _cd_m = divmod(_cd_rem // 60, 60) {{ pi }}_cd_str = f'{_cd_d}d {_cd_h:02d}h {_cd_m:02d}m' {% else %} {{ pi }}_cd_d, _cd_rem = divmod(int(_cd_secs), 86400) {{ pi }}_cd_h = _cd_rem // 3600 {{ pi }}_cd_str = f'{_cd_d}d {_cd_h}h' {% endif %} {{ pi }}self.display_manager.draw_text( {{ pi }} _cd_str, {{ pi }} x={{ el.x_expr }}, y={{ el.y_expr }}, {{ pi }} color={{ el.rgb_tuple }}, {{ pi }} font=self.display_manager.{{ el.font_attr }}, {{ pi }}) {% elif el.type == 'rectangle' %} {{ pi }}self.display_manager.draw.rectangle( {{ pi }} [{{ el.x_expr }}, {{ el.y_expr }}, {{ el.x2_expr }}, {{ el.y2_expr }}], {{ pi }} fill={{ el.fill_tuple }}, {{ pi }} outline={{ el.outline_tuple }}, {{ pi }}) {% elif el.type == 'arc' %} {{ pi }}self.display_manager.draw.arc( {{ pi }} [{{ el.x_expr }}, {{ el.y_expr }}, {{ el.x2_expr }}, {{ el.y2_expr }}], {{ pi }} start={{ el.start_angle }}, end={{ el.end_angle }}, {{ pi }} fill={{ el.rgb_tuple }}, {{ pi }} width={{ el.line_width }}, {{ pi }}) {% elif el.type == 'ellipse' %} {{ pi }}self.display_manager.draw.ellipse( {{ pi }} [{{ el.x_expr }}, {{ el.y_expr }}, {{ el.x2_expr }}, {{ el.y2_expr }}], {{ pi }} fill={{ el.fill_tuple }}, {{ pi }} outline={{ el.outline_tuple }}, {{ pi }}) {% elif el.type == 'pixel' %} {{ pi }}self.display_manager.draw.point( {{ pi }} [{{ el.x_expr }}, {{ el.y_expr }}], {{ pi }} fill={{ el.rgb_tuple }}, {{ pi }}) {% elif el.type == 'rounded_rectangle' %} {{ pi }}self.display_manager.draw.rounded_rectangle( {{ pi }} [{{ el.x_expr }}, {{ el.y_expr }}, {{ el.x2_expr }}, {{ el.y2_expr }}], {{ pi }} radius={{ el.border_radius }}, {{ pi }} fill={{ el.fill_tuple }}, {{ pi }} outline={{ el.outline_tuple }}, {{ pi }}) {% elif el.type in ('line', 'divider') %} {{ pi }}self.display_manager.draw.line( {{ pi }} [{{ el.x0_expr }}, {{ el.y0_expr }}, {{ el.x1_expr }}, {{ el.y1_expr }}], {{ pi }} fill={{ el.rgb_tuple }}, {{ pi }} width={{ el.line_width }}, {{ pi }}) {% elif el.type == 'pips' %} {{ pi }}_pip_filled = max(0, min({{ el.pip_count }}, int(float(values.get({{ el.binding_key | tojson }}, 0) or 0)))) {{ pi }}for _pip_i in range({{ el.pip_count }}): {{ pi }} _pip_x = ({{ el.x_expr }}) + _pip_i * ({{ el.pip_size }} + {{ el.pip_spacing }}) {{ pi }} _pip_color = {{ el.fill_tuple }} if _pip_i < _pip_filled else {{ el.empty_tuple }} {% if not el.show_empty %} {{ pi }} if _pip_i >= _pip_filled: {{ pi }} continue {% endif %} {{ pi }} self.display_manager.draw.rectangle( {{ pi }} [_pip_x, {{ el.y_expr }}, _pip_x + {{ el.pip_size }} - 1, ({{ el.y_expr }}) + {{ el.pip_size }} - 1], {{ pi }} fill=_pip_color, {{ pi }} ) {% elif el.type == 'sparkline' %} {{ pi }}_sl_raw = str(values.get({{ el.binding_key | tojson }}, '') or '') {{ pi }}_sl_vals = [float(v.strip()) for v in _sl_raw.split(',') if v.strip()][:{{ el.bar_count }}] {{ pi }}_sl_vals += [0.0] * max(0, {{ el.bar_count }} - len(_sl_vals)) {{ pi }}_sl_max = max(_sl_vals) if any(_sl_vals) else 1.0 {{ pi }}_sl_bw = max(1, ({{ el.bar_width_px }} - {{ el.bar_spacing }} * ({{ el.bar_count }} - 1)) // {{ el.bar_count }}) {% if el.bg_tuple != 'None' %} {{ pi }}self.display_manager.draw.rectangle( {{ pi }} [{{ el.x_expr }}, {{ el.y_expr }}, ({{ el.x_expr }}) + {{ el.bar_width_px }}, ({{ el.y_expr }}) + {{ el.bar_height_px }}], {{ pi }} fill={{ el.bg_tuple }}, {{ pi }}) {% endif %} {{ pi }}for _sl_i, _sl_v in enumerate(_sl_vals): {{ pi }} _sl_norm = max(0.0, min(1.0, _sl_v / (_sl_max or 1))) {{ pi }} _sl_bh = max(1, round({{ el.bar_height_px }} * _sl_norm)) {{ pi }} _sl_bx = ({{ el.x_expr }}) + (_sl_bw + {{ el.bar_spacing }}) * _sl_i {{ pi }} _sl_by = ({{ el.y_expr }}) + {{ el.bar_height_px }} - _sl_bh {{ pi }} self.display_manager.draw.rectangle( {{ pi }} [_sl_bx, _sl_by, _sl_bx + _sl_bw - 1, _sl_by + _sl_bh - 1], {{ pi }} fill={{ el.fill_tuple }}, {{ pi }} ) {% elif el.type == 'gauge' %} {{ pi }}_gv = max(0.0, min(100.0, float(values.get({{ el.binding_key | tojson }}, 0) or 0))) {{ pi }}_g_total = (({{ el.end_angle }} - {{ el.start_angle }}) % 360) or 360 {{ pi }}_g_sweep = _g_total * _gv / 100.0 {% if el.track_tuple != 'None' %} {{ pi }}self.display_manager.draw.arc( {{ pi }} [{{ el.x_expr }}, {{ el.y_expr }}, {{ el.x2_expr }}, {{ el.y2_expr }}], {{ pi }} start={{ el.start_angle }}, end={{ el.start_angle }} + _g_total, {{ pi }} fill={{ el.track_tuple }}, {{ pi }} width={{ el.line_width }}, {{ pi }}) {% endif %} {{ pi }}if _g_sweep > 0: {{ pi }} self.display_manager.draw.arc( {{ pi }} [{{ el.x_expr }}, {{ el.y_expr }}, {{ el.x2_expr }}, {{ el.y2_expr }}], {{ pi }} start={{ el.start_angle }}, end={{ el.start_angle }} + _g_sweep, {{ pi }} fill={{ el.rgb_tuple }}, {{ pi }} width={{ el.line_width }}, {{ pi }} ) {% if el.show_label %} {{ pi }}_g_cx = ({{ el.x_expr }}) + ({{ el.x2_expr }} - ({{ el.x_expr }})) // 2 {{ pi }}_g_cy = ({{ el.y_expr }}) + ({{ el.y2_expr }} - ({{ el.y_expr }})) // 2 {{ pi }}self.display_manager.draw_text( {{ pi }} f'{int(_gv)}%', {{ pi }} x=_g_cx, y=_g_cy, {{ pi }} color={{ el.label_tuple }}, {{ pi }} font=self.display_manager.{{ el.font_attr }}, {{ pi }}) {% endif %} {% elif el.type == 'marquee' %} {{ pi }}_{{ el.data_key }}_text = {{ el.text | tojson }} {{ pi }}_{{ el.data_key }}_tw = len(_{{ el.data_key }}_text) * {{ el.char_w }} {{ pi }}_{{ el.data_key }}_x = int(self._data.get({{ el.data_key | tojson }}, width)) {% if el.direction == 'right' %} {{ pi }}_{{ el.data_key }}_x += {{ el.scroll_speed }} {{ pi }}if _{{ el.data_key }}_x > width: {{ pi }} _{{ el.data_key }}_x = -(_{{ el.data_key }}_tw + {{ el.gap }}) {% else %} {{ pi }}_{{ el.data_key }}_x -= {{ el.scroll_speed }} {{ pi }}if _{{ el.data_key }}_x < -(_{{ el.data_key }}_tw + {{ el.gap }}): {{ pi }} _{{ el.data_key }}_x = width {% endif %} {{ pi }}self._data[{{ el.data_key | tojson }}] = _{{ el.data_key }}_x {{ pi }}self.display_manager.draw_text( {{ pi }} _{{ el.data_key }}_text, {{ pi }} x=_{{ el.data_key }}_x, y={{ el.y_expr }}, {{ pi }} color={{ el.rgb_tuple }}, {{ pi }} font=self.display_manager.{{ el.font_attr }}, {{ pi }}) {% elif el.type == 'progress_bar' %} {{ pi }}_pb_x = {{ el.x_expr }} {{ pi }}_pb_y = {{ el.y_expr }} {{ pi }}_pb_pct = max(0.0, min(100.0, float(values.get({{ el.binding_key | tojson }}, 0) or 0))) / 100.0 {{ pi }}_pb_fill_w = int({{ el.bar_width }} * _pb_pct) {{ pi }}self.display_manager.draw.rectangle( {{ pi }} [_pb_x, _pb_y, _pb_x + {{ el.bar_width }}, _pb_y + {{ el.bar_height }}], {{ pi }} fill={{ el.bg_tuple }}, {{ pi }} outline={{ el.outline_tuple }}, {{ pi }}) {{ pi }}if _pb_fill_w > 0: {{ pi }} self.display_manager.draw.rectangle( {{ pi }} [_pb_x, _pb_y, _pb_x + _pb_fill_w, _pb_y + {{ el.bar_height }}], {{ pi }} fill={{ el.fill_tuple }}, {{ pi }} ) {% endif %} {% endfor %} # ── End elements ─────────────────────────────────────────────── self.display_manager.update_display() except Exception as e: self.logger.error('Display error: %s', e, exc_info=True)