Skip to content

Commit 77710af

Browse files
Lexonight1claude
andcommitted
refactor: route all API endpoints through CommandBus + fix test fixtures
- Delete _led_route() anti-pattern; all 9 LED endpoints now dispatch through TrccApp.get().led_bus with typed commands (SetLEDModeCommand, ToggleLEDCommand, SetZoneSyncCommand, etc.) - themes.py load_theme: direct load_theme_by_name() → LoadThemeByNameCommand - devices.py select_device: direct load_last_theme() → RestoreLastThemeCommand - display.py: remaining direct dispatcher calls replaced with bus dispatch Tests: - conftest.py: replace MagicMock(spec=CommandBus) with build_lcd_bus(mock_lcd) so fixtures return real CommandResult objects, not garbage MagicMocks - test_api_buses.py: rewritten — resolution from FBL_PROFILES/SCSI_DEVICES, payload round-trip verified (not just call presence) - test_api.py: wire _lcd_bus/_led_bus in all setUp methods that hit the bus; replace all hardcoded DeviceInfo(vid=0x0402) with _scsi_dev() helper derived from SCSI_DEVICES + FBL_PROFILES (single source of truth); fix assertions for LEDMode enum values, toggle_global(False), sync interval Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4fefda0 commit 77710af

12 files changed

Lines changed: 415 additions & 197 deletions

File tree

src/trcc/api/devices.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,14 @@ def select_device(device_id: int) -> dict:
133133
api.set_current_image(ImageService.solid_color(0, 0, 0, w_res, h_res))
134134

135135
lcd.restore_device_settings()
136-
result = lcd.load_last_theme()
137-
if result.get("image"):
138-
api.set_current_image(result["image"])
139-
log.info("Restored last theme for preview")
136+
try:
137+
from trcc.core.commands.lcd import RestoreLastThemeCommand
138+
result = TrccApp.get().lcd_bus.dispatch(RestoreLastThemeCommand()).payload
139+
if result.get("image"):
140+
api.set_current_image(result["image"])
141+
log.info("Restored last theme for preview")
142+
except RuntimeError:
143+
pass
140144

141145
# Mount static file directories for this device's resolution
142146
w, h = dev.resolution or (0, 0)

src/trcc/api/display.py

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,6 @@ def _get_display():
4242
return _display_dispatcher
4343

4444

45-
def _display_frame_route(method: str, *args, **kwargs) -> dict:
46-
"""Route to lcd.frame capability, stop video/overlay first."""
47-
from trcc.api import stop_overlay_loop, stop_video_playback
48-
49-
stop_video_playback()
50-
stop_overlay_loop()
51-
result = getattr(_get_display().frame, method)(*args, **kwargs)
52-
return dispatch_result(result)
53-
54-
55-
def _display_settings_route(method: str, *args, **kwargs) -> dict:
56-
"""Route to lcd.settings capability."""
57-
result = getattr(_get_display().settings, method)(*args, **kwargs)
58-
return dispatch_result(result)
59-
6045

6146
@router.post("/color")
6247
def set_color(body: HexColorRequest) -> dict:
@@ -68,8 +53,8 @@ def set_color(body: HexColorRequest) -> dict:
6853
stop_video_playback()
6954
stop_overlay_loop()
7055
r, g, b = parse_hex_or_400(body.hex)
71-
lcd = _get_display()
72-
result = TrccApp.get().build_lcd_bus(lcd).dispatch(SendColorCommand(r=r, g=g, b=b))
56+
_get_display()
57+
result = TrccApp.get().lcd_bus.dispatch(SendColorCommand(r=r, g=g, b=b))
7358
return dispatch_result(result.payload)
7459

7560

@@ -79,8 +64,8 @@ def set_brightness(body: BrightnessRequest) -> dict:
7964
from trcc.core.app import TrccApp
8065
from trcc.core.commands.lcd import SetBrightnessCommand
8166

82-
lcd = _get_display()
83-
result = TrccApp.get().build_lcd_bus(lcd).dispatch(SetBrightnessCommand(level=body.level))
67+
_get_display()
68+
result = TrccApp.get().lcd_bus.dispatch(SetBrightnessCommand(level=body.level))
8469
return dispatch_result(result.payload)
8570

8671

@@ -90,8 +75,8 @@ def set_rotation(body: RotationRequest) -> dict:
9075
from trcc.core.app import TrccApp
9176
from trcc.core.commands.lcd import SetRotationCommand
9277

93-
lcd = _get_display()
94-
result = TrccApp.get().build_lcd_bus(lcd).dispatch(SetRotationCommand(degrees=body.degrees))
78+
_get_display()
79+
result = TrccApp.get().lcd_bus.dispatch(SetRotationCommand(degrees=body.degrees))
9580
return dispatch_result(result.payload)
9681

9782

@@ -101,15 +86,23 @@ def set_split(body: SplitRequest) -> dict:
10186
from trcc.core.app import TrccApp
10287
from trcc.core.commands.lcd import SetSplitModeCommand
10388

104-
lcd = _get_display()
105-
result = TrccApp.get().build_lcd_bus(lcd).dispatch(SetSplitModeCommand(mode=body.mode))
89+
_get_display()
90+
result = TrccApp.get().lcd_bus.dispatch(SetSplitModeCommand(mode=body.mode))
10691
return dispatch_result(result.payload)
10792

10893

10994
@router.post("/reset")
11095
def reset_display() -> dict:
11196
"""Reset device by sending solid red frame."""
112-
return _display_frame_route("reset")
97+
from trcc.api import stop_overlay_loop, stop_video_playback
98+
from trcc.core.app import TrccApp
99+
from trcc.core.commands.lcd import ResetDisplayCommand
100+
101+
stop_video_playback()
102+
stop_overlay_loop()
103+
_get_display()
104+
result = TrccApp.get().lcd_bus.dispatch(ResetDisplayCommand())
105+
return dispatch_result(result.payload)
113106

114107

115108
@router.post("/mask")
@@ -118,7 +111,7 @@ async def load_mask(image: UploadFile) -> dict:
118111
import tempfile
119112
from pathlib import Path
120113

121-
lcd = _get_display()
114+
_get_display()
122115

123116
data = await image.read()
124117
if len(data) > 10 * 1024 * 1024:
@@ -130,8 +123,10 @@ async def load_mask(image: UploadFile) -> dict:
130123
tmp_path = tmp.name
131124

132125
try:
133-
result = lcd.load_mask_standalone(tmp_path)
134-
return dispatch_result(result)
126+
from trcc.core.app import TrccApp
127+
from trcc.core.commands.lcd import LoadMaskCommand
128+
result = TrccApp.get().lcd_bus.dispatch(LoadMaskCommand(mask_path=tmp_path))
129+
return dispatch_result(result.payload)
135130
finally:
136131
Path(tmp_path).unlink(missing_ok=True)
137132

@@ -153,12 +148,13 @@ async def render_overlay(dc_path: str, send: bool = True) -> dict:
153148
if not safe_path.startswith(allowed_dir + os.sep) and safe_path != allowed_dir:
154149
raise HTTPException(status_code=400, detail="Invalid overlay path")
155150

156-
import trcc.api as api
151+
from trcc.core.app import TrccApp
152+
from trcc.core.commands.lcd import RenderOverlayFromDCCommand
157153

158-
lcd = _get_display()
159-
metrics = api._system_svc.all_metrics if api._system_svc is not None else None
160-
result = lcd.render_overlay_from_dc(safe_path, send=send, metrics=metrics)
161-
return dispatch_result(result)
154+
_get_display()
155+
result = TrccApp.get().lcd_bus.dispatch(
156+
RenderOverlayFromDCCommand(dc_path=safe_path, send=send))
157+
return dispatch_result(result.payload)
162158

163159

164160
@router.get("/status")
@@ -252,11 +248,14 @@ def test_display() -> dict:
252248
(255, 255, 255, "White"),
253249
]
254250

251+
from trcc.core.app import TrccApp
252+
from trcc.core.commands.lcd import SendColorCommand
255253
from trcc.services import ImageService
256254

255+
bus = TrccApp.get().lcd_bus
257256
for r, g, b, _name in colors:
258257
img = ImageService.solid_color(r, g, b, w, h)
259-
lcd.frame.send_color(r, g, b)
258+
bus.dispatch(SendColorCommand(r=r, g=g, b=b))
260259
time.sleep(1)
261260

262261
# Update preview with last frame
@@ -406,10 +405,13 @@ async def create_theme(
406405
raise HTTPException(status_code=400, detail="Failed to open background image")
407406

408407
if mask_path:
409-
result = lcd.load_mask_standalone(str(mask_path))
410-
if not result.get("success"):
411-
raise HTTPException(status_code=400, detail=result.get("error", "Mask load failed"))
412-
img = result.get("image", img)
408+
from trcc.core.app import TrccApp
409+
from trcc.core.commands.lcd import LoadMaskCommand
410+
mask_result = TrccApp.get().lcd_bus.dispatch(
411+
LoadMaskCommand(mask_path=str(mask_path))).payload
412+
if not mask_result.get("success"):
413+
raise HTTPException(status_code=400, detail=mask_result.get("error", "Mask load failed"))
414+
img = mask_result.get("image", img)
413415

414416
if overlay_config:
415417
from trcc.adapters.infra.dc_config import DcConfig
@@ -427,10 +429,10 @@ async def create_theme(
427429
api._overlay_svc = overlay_svc
428430
from trcc.services.system import get_all_metrics
429431
frame = overlay_svc.render(get_all_metrics())
430-
lcd.frame.send_frame(frame)
432+
lcd.send(frame)
431433
api.set_current_image(frame)
432434
else:
433-
lcd.frame.send_frame(img)
435+
lcd.send(img)
434436
api.set_current_image(img)
435437

436438
return {"success": True, "animated": False, "resolution": f"{w}x{h}"}

src/trcc/api/led.py

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@ def _get_led():
3232
return _led_dispatcher
3333

3434

35-
def _led_route(method: str, *args, **kwargs) -> dict:
36-
"""Generic: get LEDDevice, call method, return dispatch result."""
37-
return dispatch_result(getattr(_get_led(), method)(*args, **kwargs))
38-
39-
4035
# ── Global operations ──────────────────────────────────────────────────
4136

4237
@router.post("/color")
@@ -48,17 +43,28 @@ def set_color(body: HexColorRequest) -> dict:
4843

4944
stop_led_loop()
5045
r, g, b = parse_hex_or_400(body.hex)
51-
led = _get_led()
52-
result = TrccApp.get().build_led_bus(led).dispatch(SetLEDColorCommand(r=r, g=g, b=b))
46+
_get_led()
47+
result = TrccApp.get().led_bus.dispatch(SetLEDColorCommand(r=r, g=g, b=b))
5348
return dispatch_result(result.payload)
5449

5550

5651
@router.post("/mode")
5752
def set_mode(body: ModeRequest) -> dict:
5853
"""Set LED effect mode (static, breathing, colorful, rainbow, temp_linked, load_linked)."""
5954
from trcc.api import start_led_loop, stop_led_loop
55+
from trcc.core.app import TrccApp
56+
from trcc.core.commands.led import SetLEDModeCommand
57+
from trcc.core.models import LEDMode
6058

61-
result = _led_route("set_mode", body.mode)
59+
_get_led()
60+
try:
61+
mode = LEDMode[body.mode.upper()]
62+
except KeyError:
63+
raise HTTPException(
64+
status_code=400,
65+
detail=f"Unknown mode '{body.mode}'. Choose: {', '.join(m.name.lower() for m in LEDMode)}",
66+
)
67+
result = dispatch_result(TrccApp.get().led_bus.dispatch(SetLEDModeCommand(mode=mode)).payload)
6268
if result.get("animated"):
6369
start_led_loop()
6470
else:
@@ -72,18 +78,22 @@ def set_brightness(body: LEDBrightnessRequest) -> dict:
7278
from trcc.core.app import TrccApp
7379
from trcc.core.commands.led import SetLEDBrightnessCommand
7480

75-
led = _get_led()
76-
result = TrccApp.get().build_led_bus(led).dispatch(SetLEDBrightnessCommand(level=body.level))
81+
_get_led()
82+
result = TrccApp.get().led_bus.dispatch(SetLEDBrightnessCommand(level=body.level))
7783
return dispatch_result(result.payload)
7884

7985

8086
@router.post("/off")
8187
def turn_off() -> dict:
8288
"""Turn LEDs off."""
8389
from trcc.api import stop_led_loop
90+
from trcc.core.app import TrccApp
91+
from trcc.core.commands.led import ToggleLEDCommand
8492

8593
stop_led_loop()
86-
return _led_route("off")
94+
_get_led()
95+
result = TrccApp.get().led_bus.dispatch(ToggleLEDCommand(on=False))
96+
return dispatch_result(result.payload)
8797

8898

8999
@router.post("/sensor")
@@ -92,8 +102,8 @@ def set_sensor(body: LEDSensorRequest) -> dict:
92102
from trcc.core.app import TrccApp
93103
from trcc.core.commands.led import SetLEDSensorSourceCommand
94104

95-
led = _get_led()
96-
result = TrccApp.get().build_led_bus(led).dispatch(SetLEDSensorSourceCommand(source=body.source))
105+
_get_led()
106+
result = TrccApp.get().led_bus.dispatch(SetLEDSensorSourceCommand(source=body.source))
97107
return dispatch_result(result.payload)
98108

99109

@@ -106,53 +116,96 @@ def set_zone_color(zone: int, body: HexColorRequest) -> dict:
106116
from trcc.core.commands.led import SetZoneColorCommand
107117

108118
r, g, b = parse_hex_or_400(body.hex)
109-
led = _get_led()
110-
result = TrccApp.get().build_led_bus(led).dispatch(SetZoneColorCommand(zone=zone, r=r, g=g, b=b))
119+
_get_led()
120+
result = TrccApp.get().led_bus.dispatch(SetZoneColorCommand(zone=zone, r=r, g=g, b=b))
111121
return dispatch_result(result.payload)
112122

113123

114124
@router.post("/zones/{zone}/mode")
115125
def set_zone_mode(zone: int, body: ModeRequest) -> dict:
116126
"""Set effect mode for a specific LED zone."""
117-
return _led_route("set_zone_mode", zone, body.mode)
127+
from trcc.core.app import TrccApp
128+
from trcc.core.commands.led import SetZoneModeCommand
129+
from trcc.core.models import LEDMode
130+
131+
_get_led()
132+
try:
133+
mode = LEDMode[body.mode.upper()]
134+
except KeyError:
135+
raise HTTPException(
136+
status_code=400,
137+
detail=f"Unknown mode '{body.mode}'. Choose: {', '.join(m.name.lower() for m in LEDMode)}",
138+
)
139+
result = TrccApp.get().led_bus.dispatch(SetZoneModeCommand(zone=zone, mode=mode))
140+
return dispatch_result(result.payload)
118141

119142

120143
@router.post("/zones/{zone}/brightness")
121144
def set_zone_brightness(zone: int, body: LEDBrightnessRequest) -> dict:
122145
"""Set brightness for a specific LED zone (0-100)."""
123-
return _led_route("set_zone_brightness", zone, body.level)
146+
from trcc.core.app import TrccApp
147+
from trcc.core.commands.led import SetZoneBrightnessCommand
148+
149+
_get_led()
150+
result = TrccApp.get().led_bus.dispatch(SetZoneBrightnessCommand(zone=zone, level=body.level))
151+
return dispatch_result(result.payload)
124152

125153

126154
@router.post("/zones/{zone}/toggle")
127155
def toggle_zone(zone: int, body: ToggleRequest) -> dict:
128156
"""Toggle a specific LED zone on/off."""
129-
return _led_route("toggle_zone", zone, body.on)
157+
from trcc.core.app import TrccApp
158+
from trcc.core.commands.led import ToggleZoneCommand
159+
160+
_get_led()
161+
result = TrccApp.get().led_bus.dispatch(ToggleZoneCommand(zone=zone, on=body.on))
162+
return dispatch_result(result.payload)
130163

131164

132165
@router.post("/sync")
133166
def set_sync(body: ZoneSyncRequest) -> dict:
134167
"""Enable/disable zone sync (circulate/select-all)."""
135-
return _led_route("set_zone_sync", body.enabled, body.interval)
168+
from trcc.core.app import TrccApp
169+
from trcc.core.commands.led import SetZoneSyncCommand
170+
171+
_get_led()
172+
result = TrccApp.get().led_bus.dispatch(SetZoneSyncCommand(enabled=body.enabled, interval=body.interval or 0))
173+
return dispatch_result(result.payload)
136174

137175

138176
# ── Segment operations ─────────────────────────────────────────────────
139177

140178
@router.post("/segments/{index}/toggle")
141179
def toggle_segment(index: int, body: ToggleRequest) -> dict:
142180
"""Toggle a specific LED segment on/off."""
143-
return _led_route("toggle_segment", index, body.on)
181+
from trcc.core.app import TrccApp
182+
from trcc.core.commands.led import ToggleSegmentCommand
183+
184+
_get_led()
185+
result = TrccApp.get().led_bus.dispatch(ToggleSegmentCommand(index=index, on=body.on))
186+
return dispatch_result(result.payload)
144187

145188

146189
@router.post("/clock")
147190
def set_clock(body: ClockFormatRequest) -> dict:
148191
"""Set LED segment display clock format (12h/24h)."""
149-
return _led_route("set_clock_format", body.is_24h)
192+
from trcc.core.app import TrccApp
193+
from trcc.core.commands.led import SetClockFormatCommand
194+
195+
_get_led()
196+
result = TrccApp.get().led_bus.dispatch(SetClockFormatCommand(is_24h=body.is_24h))
197+
return dispatch_result(result.payload)
150198

151199

152200
@router.post("/temp-unit")
153201
def set_temp_unit(body: TempUnitRequest) -> dict:
154202
"""Set LED segment display temperature unit (C/F)."""
155-
return _led_route("set_temp_unit", body.unit)
203+
from trcc.core.app import TrccApp
204+
from trcc.core.commands.led import SetTempUnitLEDCommand
205+
206+
_get_led()
207+
result = TrccApp.get().led_bus.dispatch(SetTempUnitLEDCommand(unit=body.unit))
208+
return dispatch_result(result.payload)
156209

157210

158211
# ── Test endpoint ─────────────────────────────────────────────────────

src/trcc/api/themes.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,11 @@ def load_theme(body: ThemeLoadRequest) -> dict:
237237
if body.resolution:
238238
w, h = _parse_resolution(body.resolution)
239239

240-
result = api._display_dispatcher.load_theme_by_name(body.name, w, h)
240+
from trcc.core.app import TrccApp
241+
from trcc.core.commands.lcd import LoadThemeByNameCommand
242+
243+
result = TrccApp.get().lcd_bus.dispatch(
244+
LoadThemeByNameCommand(name=body.name, width=w, height=h)).payload
241245
if not result.get("success"):
242246
return dispatch_result(result)
243247

0 commit comments

Comments
 (0)