From 148862621a9b4916f3dac8457a811088eb9fe9ce Mon Sep 17 00:00:00 2001 From: twfarley Date: Thu, 10 Sep 2026 19:43:48 -0600 Subject: [PATCH] Group switch tile channels under device headings --- docs/user_guide.md | 2 +- sensorius/__init__.py | 2 +- sensorius/saiHtml.py | 40 +++++++++++++++++++--------------------- ui_static/css/app.css | 6 ++++++ 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/docs/user_guide.md b/docs/user_guide.md index 92c46b6..e0f791f 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -202,7 +202,7 @@ The technical sensor or switch ID remains the stable identity used by settings, ### Switch Tiles -Switch Tiles show local Raspberry Pi relay channels and remote Nodus switch channels through the same interface. Each Switch Tile represents one channel and has a label, current state, and recent state changes. Sensorius groups Switch Tiles under their switch-device heading. Manual toggles send commands through the shared switch controller. For Nodus switches, commands are sent through MQTT and Sensorius waits for state to return from the device. +Switch Tiles show local Raspberry Pi relay channels and remote Nodus switch channels through the same interface. Each Switch Tile represents one channel and has a label, current state, and recent state changes. Sensorius groups switches by location. When multiple devices share a location, the title identifies the first device and shows its settings gear. Each additional device has a centered, full-width divider with its ID, settings gear, and location above its channel labels, styled to match the title. Manual toggles send commands through the shared switch controller. For Nodus switches, commands are sent through MQTT and Sensorius waits for state to return from the device. If an enabled Advanced automation owns a switch channel, Sensorius blocks manual toggles for that channel so the automation remains in control. diff --git a/sensorius/__init__.py b/sensorius/__init__.py index d06c6ca..ce18a10 100644 --- a/sensorius/__init__.py +++ b/sensorius/__init__.py @@ -4,4 +4,4 @@ release notes, and supporting tooling can report a consistent build identity. """ -__version__ = "v0.26.253.5" +__version__ = "v0.26.253.8" diff --git a/sensorius/saiHtml.py b/sensorius/saiHtml.py index eddd3f9..e7f6a88 100644 --- a/sensorius/saiHtml.py +++ b/sensorius/saiHtml.py @@ -1297,7 +1297,6 @@ def _render_switch_cards_for_location(location: str, matched_switches: list): rendered_swids_here: set[str] = set() switch_rows: list[dict] = [] switch_ids_here: list[str] = [] - label_counts: dict[str, int] = defaultdict(int) for switch_ctrl in matched_switches: sw_id: str = (getattr(switch_ctrl, "switch_id", "") or "").strip() @@ -1398,7 +1397,6 @@ def _render_switch_cards_for_location(location: str, matched_switches: list): continue for label in render_labels: - label_counts[str(label or "").strip().lower()] += 1 switch_rows.append({ "empty": False, "switch_ctrl": switch_ctrl, @@ -1414,16 +1412,15 @@ def _render_switch_cards_for_location(location: str, matched_switches: list): header_id = _safe(f"{'_'.join(switch_ids_here) if switch_ids_here else location}_header") yield f"
" yield "
" - if not multi_switch_card and switch_ids_here: + if switch_ids_here: header_sw_id = switch_ids_here[0] - yield f"

{header_sw_id.upper()} " + yield f"

{header_sw_id.upper()} " yield f" " yield from _settings_gear_svg_lines(indent=" ") yield " " yield f"{location}

" else: - header_devices = ", ".join(sw.upper() for sw in switch_ids_here if sw) - yield f"

SWITCHES {header_devices} {location}

" + yield f"

SWITCHES {location}

" yield "
" yield "
" @@ -1434,15 +1431,28 @@ def _render_switch_cards_for_location(location: str, matched_switches: list): yield "" yield "" - if not any(not row.get("empty") for row in switch_rows): + if not multi_switch_card and not any(not row.get("empty") for row in switch_rows): yield "No enabled switch channels" + previous_device_id = switch_rows[0]["sw_id"] for row in switch_rows: + sw_id = row["sw_id"] + if multi_switch_card and sw_id != previous_device_id: + previous_device_id = sw_id + yield "

" + yield html_escape(sw_id.upper() or "Unknown device") + if sw_id: + settings_action = html_escape(f"editSwitchSettings({json.dumps(sw_id)})") + yield f" " + yield from _settings_gear_svg_lines(indent="", aria_hidden=True) + yield "" + yield f" {html_escape(location)}

" if row.get("empty"): + if multi_switch_card: + yield "No enabled switch channels" continue switch_ctrl = row["switch_ctrl"] - sw_id = row["sw_id"] label = row["label"] safe_label = label.lower().replace(" ", "_") is_on = bool(getattr(switch_ctrl, "last_state", {}).get(label, False)) @@ -1500,19 +1510,7 @@ def _render_switch_cards_for_location(location: str, matched_switches: list): if automation_enabled: state_cell_classes += " automation-enabled" - label_key = label_norm.lower() - display_label = label - if label_counts.get(label_key, 0) > 1 and sw_id: - display_label = f"{label} ({sw_id.upper()})" - - label_cell = html_escape(display_label) - if multi_switch_card and sw_id: - label_cell += ( - f" " - ) - label_cell += "".join(_settings_gear_svg_lines(indent="", aria_hidden=True)) - label_cell += "" + label_cell = html_escape(label) yield "" yield f"{label_cell}" diff --git a/ui_static/css/app.css b/ui_static/css/app.css index af838f7..6efc619 100644 --- a/ui_static/css/app.css +++ b/ui_static/css/app.css @@ -634,6 +634,12 @@ body.dashboard-page #sensor_header{ background-color:var(--dashboard-card-bg); color:var(--dashboard-card-text); } .switch-table th{ font-weight:bold; } +.switch-device-title{ font-size:1.17rem; font-weight:bold; } +.switch-device-divider .switch-device-title{ margin:0; } +.switch-table .switch-device-divider th{ + text-align:center; overflow-wrap:anywhere; + border-top:2px solid var(--dashboard-card-border); +} .switch-state-cell{ display:flex; flex-direction:column; align-items:center; justify-content:flex-start; gap:.05rem;