# Bind Schedule Device Dispatch Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** When the app binds a schedule to one or more devices, immediately publish that schedule's detail payload to each bound device. **Architecture:** Keep the change inside the existing app module flow. `AppController.addScheduleDevice` remains the entry point, persists the relation as it does today, then reuses the existing `IDeviceCommandService.sendScheduleBindCommand(...)` path to publish a `bindSchedule` command with a small payload assembled from current schedule-detail data. Shared payload shaping should stay private to `AppController` unless implementation pressure proves otherwise. **Tech Stack:** Java 17, Spring Boot 3, existing app controller/service layer, MQTT command publishing through `IDeviceCommandService`, Maven ## Global Constraints - Only touch the schedule-device binding flow in `water-modules/water-app` - Reuse existing `IDeviceCommandService.sendScheduleBindCommand(...)` - Do not modify MQTT publisher, ACK handling, or topic routing - Published payload must contain `deviceNo` and `details` - `details` must follow the current device-facing schedule detail structure used by `buildScheduleBindPayload` - Binding uses synchronous failure semantics: command publish failure must fail the API call - Keep changes ASCII unless the target file already uses non-ASCII --- ### Task 1: Add a focused regression test for schedule-device bind dispatch **Files:** - Create: `water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java` - Modify: `water-modules/water-app/pom.xml` **Interfaces:** - Consumes: `AppController.addScheduleDevice(String body)` - Produces: a regression test proving that binding a device triggers `deviceCommandService.sendScheduleBindCommand(deviceNo, payload)` - [ ] **Step 1: Add the test dependency baseline for the app module** ```xml org.springframework.boot spring-boot-starter-test test ``` - [ ] **Step 2: Write the failing controller regression test** ```java package org.dromara.app.controller; import org.dromara.app.domain.bo.AppSchedulingDeviceBo; import org.dromara.app.domain.vo.AppDeviceVo; import org.dromara.app.domain.vo.AppScheduleDetailVo; import org.dromara.app.domain.vo.AppScheduleVo; import org.dromara.app.service.IAppDeviceService; import org.dromara.app.service.IAppScheduleDetailService; import org.dromara.app.service.IAppScheduleService; import org.dromara.app.service.IAppSchedulingDeviceService; import org.dromara.app.service.IAppWateringLogService; import org.dromara.app.service.IDeviceCommandService; import org.dromara.common.core.domain.R; import org.dromara.system.service.ISysUserService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class AppControllerTest { @Mock private IAppDeviceService appDeviceService; @Mock private IAppScheduleService appScheduleService; @Mock private IAppScheduleDetailService appScheduleDetailService; @Mock private org.dromara.app.service.impl.AppScheduleServiceImpl appScheduleServiceimpl; @Mock private IAppSchedulingDeviceService appSchedulingDeviceService; @Mock private ISysUserService userService; @Mock private IAppWateringLogService appWateringLogService; @Mock private IDeviceCommandService deviceCommandService; @InjectMocks private AppController controller; @Test void addScheduleDevice_dispatchesSchedulePayloadAfterBinding() { AppScheduleVo schedule = new AppScheduleVo(); schedule.setId(10L); schedule.setUserId(99L); schedule.setName("Morning"); schedule.setStatus("1"); AppDeviceVo device = new AppDeviceVo(); device.setDeviceNo("D01"); device.setUserId(99L); AppScheduleDetailVo detail = new AppScheduleDetailVo(); detail.setId(101L); detail.setWeekday("1"); detail.setTimeData("[{\"startTime\":\"08:00\",\"durationMin\":15}]"); detail.setTriggerType("0"); detail.setStatus("1"); when(appScheduleService.queryById(10L)).thenReturn(schedule); when(appDeviceService.queryById("D01")).thenReturn(device); when(appScheduleDetailService.queryByScheduleIdByStatus(10L)).thenReturn(List.of(detail)); when(appSchedulingDeviceService.insertByBo(any(AppSchedulingDeviceBo.class))).thenReturn(true); when(deviceCommandService.sendScheduleBindCommand(eq("D01"), anyMap())).thenReturn("cmd-1"); R result = controller.addScheduleDevice("{\"scheduleId\":10,\"deviceNos\":[\"D01\"]}"); assertThat(result.getCode()).isEqualTo(200); ArgumentCaptor> payloadCaptor = ArgumentCaptor.forClass(Map.class); verify(deviceCommandService).sendScheduleBindCommand(eq("D01"), payloadCaptor.capture()); Map payload = payloadCaptor.getValue(); assertThat(payload.get("deviceNo")).isEqualTo("D01"); assertThat(payload).containsKey("details").doesNotContainKeys("cmd", "schedule"); } } ``` - [ ] **Step 3: Run the test to verify it fails for the expected reason** Run: `mvn -pl water-modules/water-app -DskipTests=false -Dtest=AppControllerTest test` Expected: FAIL because `sendScheduleBindCommand(...)` is never invoked and payload assertions cannot be satisfied yet - [ ] **Step 4: Commit the failing test baseline** ```bash git add water-modules/water-app/pom.xml water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java git commit -m "test: cover schedule bind device dispatch" ``` ### Task 2: Implement schedule payload assembly and dispatch in AppController **Files:** - Modify: `water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java` - Test: `water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java` **Interfaces:** - Consumes: `IAppScheduleService.queryById(Long)`, `IAppScheduleDetailService.queryByScheduleIdByStatus(Long)`, `IDeviceCommandService.sendScheduleBindCommand(String, Map)` - Produces: `addScheduleDevice(...)` publishes a `bindSchedule` command after each successful relation insert - [ ] **Step 1: Implement the minimum controller change to make the test pass** ```java for (String deviceNo : deviceNos) { if (StringUtils.isBlank(deviceNo)) { continue; } assertDeviceOwned(deviceNo); AppSchedulingDeviceBo schedulingDeviceBo = new AppSchedulingDeviceBo(); schedulingDeviceBo.setDeviceNo(deviceNo); schedulingDeviceBo.setScheduleId(scheduleId); appSchedulingDeviceService.insertByBo(schedulingDeviceBo); deviceCommandService.sendScheduleBindCommand(deviceNo, buildScheduleBindPayload(scheduleId, deviceNo)); } ``` ```java private Map buildScheduleBindPayload(Long scheduleId, String deviceNo) { List details = appScheduleDetailService.queryByScheduleIdByStatus(scheduleId); List> detailPayload = new ArrayList<>(); for (AppScheduleDetailVo detail : details) { List timeSlots = new ArrayList<>(); JSONArray timeArray = JSONUtil.parseArray(detail.getTimeData()); for (Object timeSlot : timeArray) { timeSlots.add(timeSlot); } Map item = new HashMap<>(); item.put("weekday", detail.getWeekday()); item.put("timeData", timeSlots); item.put("triggerType", detail.getTriggerType()); item.put("status", detail.getStatus()); detailPayload.add(item); } Map payload = new HashMap<>(); payload.put("deviceNo", deviceNo); payload.put("details", detailPayload); return payload; } ``` - [ ] **Step 2: Run the focused regression test to verify it passes** Run: `mvn -pl water-modules/water-app -DskipTests=false -Dtest=AppControllerTest test` Expected: PASS with `AppControllerTest` green - [ ] **Step 3: Refactor only if needed to remove duplication inside schedule bind payload shaping** ```java private List> toScheduleDetailPayload(List details) { List> detailPayload = new ArrayList<>(); for (AppScheduleDetailVo detail : details) { List timeSlots = new ArrayList<>(); JSONArray timeArray = JSONUtil.parseArray(detail.getTimeData()); for (Object timeSlot : timeArray) { timeSlots.add(timeSlot); } Map item = new HashMap<>(); item.put("weekday", detail.getWeekday()); item.put("timeData", timeSlots); item.put("triggerType", detail.getTriggerType()); item.put("status", detail.getStatus()); detailPayload.add(item); } return detailPayload; } ``` - [ ] **Step 4: Re-run the focused regression test after refactor** Run: `mvn -pl water-modules/water-app -DskipTests=false -Dtest=AppControllerTest test` Expected: PASS - [ ] **Step 5: Commit the implementation** ```bash git add water-modules/water-app/src/main/java/org/dromara/app/controller/AppController.java water-modules/water-app/src/test/java/org/dromara/app/controller/AppControllerTest.java git commit -m "feat: dispatch schedule payload when binding device" ``` ### Task 3: Run module-level verification and review edge behavior **Files:** - Modify: none required unless verification reveals defects **Interfaces:** - Consumes: completed controller change and regression test - Produces: fresh verification evidence for the implementation claim - [ ] **Step 1: Run the focused controller regression test** Run: `mvn -pl water-modules/water-app -DskipTests=false -Dtest=AppControllerTest test` Expected: PASS - [ ] **Step 2: Run a compile pass for the app module with tests skipped** Run: `mvn -pl water-modules/water-app -DskipTests compile` Expected: BUILD SUCCESS - [ ] **Step 3: Inspect the diff to confirm scope stayed local** Run: `git diff --stat` Expected: only the app controller, app module test baseline, the new controller test, and planning/spec docs changed for this task - [ ] **Step 4: Commit any verification-driven follow-up if needed** ```bash git add -A git commit -m "chore: verify schedule bind dispatch change" ```