Public: anyone can read this; the crew can change it.
Migrated from github.com/Trevato/SO-ARM100
Grow light schedule: a switch driver (smart plug, GPIO relay, or uhubctl only when it switches the light alone) with a photoperiod that runs on the Pi #631
Check · Review · Land done · Live — Landed; nothing deploys from this branch, so this is the end of its path.
`main` is already merged into `smith/582-grow-light-schedule-a-switch-driver-smar`, so I have nothing new to push. I did not commit the merge myself.
shipped merged
A uhubctl switch is now tried before it is kept, and it is saved only once the owner confirms the light went off. I ran only the type check and the light, admin and grow tests. They pass, but I didn't run the whole bun test, and none of it has run on the Pi or against a real hub.
The existing refusal for ganged hubs is untouched. A hub with real per-port power, such as the UUGear MEGA4 that trevato named, still passes verify().
- Try-out: saving a uhubctl switch now runs
trial()insrc/light-switch.ts. It runslsusb, switches the port off for 2 s, runslsusbagain and switches the port back on, even if the check throws. If a configured camera id, or a device whose name looks like a camera, disappeared, it fails with a message naming that camera. - Confirmation: a switch that passed the try-out is not saved.
Lightholds it in memory as pending, and the view showspending(for example "USB hub 1-1, port 2").
- The grow card then shows "Did the light go off?" with Yes, it went off: save and No, it stayed on.
- The card also says that a port reporting "off" is not proof on a ganged hub.
- No drops the pending switch. Pending state is lost on restart, which just means starting the setup again.
- The same step exists as POST /grow/<device> {"confirm": true|false}, with an /admin action light-confirm and a new lightTried flash for the page. Shelly and GPIO switches save as before.
- Terminal:
/grow … switch uhubctl …goes through the same try-out and now leaves the switch pending. I did not add a terminal command to confirm it, so confirming is only on the grow card or the endpoint for now.
Tests in src/light.test.ts cover:
- the try-out order (
lsusb, off,lsusb, on); - a dropped camera failing it, with the port still switched back on;
- keeping the switch only on a yes.
The repo has no scripts/design-lint.ts, so the design lint was not run. The branch carried an unfinished merge of main with no conflict markers left. I committed it together with this change, so the pull diff includes the merge.
main is already merged into smith/582-grow-light-schedule-a-switch-driver-smar, so I have nothing new to push. I did not commit the merge myself.
I resolved the conflicts in Software/so101/src/light-endpoint.test.ts and server.ts by taking main's side. It is a superset of ours: it adds the grow key header to the tests and the ArmAbsentError 503 answer in server.ts. After that, src/light-endpoint.test.ts, src/light.test.ts and src/server* passed (99 tests). I did not get a clean type check, because bun x tsc --noEmit couldn't find the bun type definitions. A full bun test run showed 64 failures. I attribute them to two test runs going at once and did not confirm that.
When I went to commit, the working tree was already clean and the branch was at dbe4207, which matches origin. origin/main is an ancestor of it, there are no conflict markers in Software/so101/src, and git push reported everything up to date. I don't know why my uncommitted merge state was gone.
I did not read what dbe4207 changed in those two files, so the pull may not match my resolution.
Fix first: the terminal door cannot finish what it starts. /grow … switch uhubctl … in src/tty.ts now goes through the try-out and leaves the switch pending, but there is no terminal command to answer "did the light go off?". So a terminal user can start the setup and never complete it. The author says so in the conversation. The list requires that whatever the page does, the tty does too, and the README to say how. Add a confirm yes/no command to the tty (owner-gated like the other /grow writes). It should call Light.confirm and reach web viewers live. Add a test for it, and document it in the README.
Also fix:
- trial() ends with set(true) whatever the schedule says. If the test runs in the spinach dark period, the light is switched ON for the rest of the dark period. Spinach counts light in its dark hours as day, and the card itself warns about this. Restore the port to the state it had before the trial, or run the schedule tick straight after. Test it with the schedule off at trial time.
- admin-routes.ts: the
done = ...ternary (switch === undefined || null || driver !== uhubctl) is unreadable. Make it a small helper such astriedFirst(body). - Tests: there is no test for POST /grow/light {confirm}, for the 400 on mixing confirm with other fields, or for the
light-confirmadmin action and the pending card. Each is new behaviour and needs one. - The pending state is memory-only. That is acceptable, but the card should say so. Say it also in the README.
What is good: the trial order and the camera-drop failure with port restored are tested. A "no" answer drops the switch. The card copy is plain and the buttons are real forms, so it works with JavaScript off.
The terminal can now answer "did the light go off?", and the uhubctl try-out puts the port back as the schedule says. I haven't run the full suite. The tests I did run pass: src/light*, src/terminal.test.ts and src/admin*, plus src/light-endpoint.test.ts again after a fix to its own assertion. bun x tsc --noEmit reports errors, none in the files I touched. bun scripts/design-lint.ts doesn't exist in Software/so101, so it wasn't run, and I didn't touch src/web/.
- Terminal confirm:
/grow [light|pump] confirm yes|noposts{confirm: bool}toPOST /grow/<device>. That is the same owner-gated route the other/growwrites use, and it callsLight.confirm./grownow prints a waiting try-out and names this command. - Live to web viewers: I did nothing special for this. The terminal write goes through the same server route as the card's forms, and a viewer sees the new state when its card next loads. The terminal path is covered by command-parsing tests only. Nothing checks that a viewer's card updates after a terminal confirm.
- Dark period:
trial()now takes the state to restore, andLight.configurepasses what the schedule wants now (on when there is no schedule). The new test sets a schedule that is off at trial time. It checks the port goes off, then off again, never on. triedFirst(body): it lives insrc/light-panel.tsand replaces the long ternary insrc/admin-routes.ts.- New tests:
- POST /grow/light {confirm}: yes and no.
- The 400 for nothing waiting, for a non-boolean confirm, and for confirm mixed with override, schedule or switch.
- The pending card: its two forms, owner-only, and the memory-only note.
- light-confirm turning into {confirm} through lightRequest.
- triedFirst.
- Memory-only: the pending card and the README both say a restart of the arm server forgets the try-out. The README also documents the new command.
I did not add a test that drives the light-confirm action through /admin end to end. It would need a stub for the light hooks, which no existing admin test has. The request mapping, triedFirst and the card are covered separately.
🤖 Generated with Claude Code
Preview boots (/ 200; /grow is 404 to a guest, as intended). Two things to fix; the first one first.
- The new /admin path has no test.
light-confirmin admin-routes and thelightTriedflash are behaviour this pull adds, and the author says no test drives them. Deletelight-confirmortriedFirst(...) ? "lightTried"and every test still passes. Add an admin test with a stubaskLight: a uhubctllight-switchform flasheslightTried, and alight-confirmform posts{confirm:true}and flasheslightSaved. Also add a test that the terminal's/grow confirm yeswrite is gated as the other /grow writes are (a guest gets nothing), since the direction asks the terminal to gate like the page.
- The try-out can leave the grow light off. In
trial(),await this.set(false)sits outside the try/finally. If uhubctl cuts the port and then returns non-zero or times out, nothing restores it, and on the live grow box that leaves the light dark with no error naming it. Putset(false)inside the try so thefinallyalways restores torestoreOn. If the restore itself throws, do not let it hide a camera-drop error: report both.
Next after that: the card is not pushed live after a terminal confirm (a viewer sees it on next load), so say so in the README; and one pending slot per device means two owners can overwrite each other's try-out, so show which switch a yes applies to.
The /admin tests and the trial() fix are pushed (07ff7dc). src/light* and src/admin-relay.test.ts pass (49 tests, 0 failures) and I did not run the full suite. A run from the repo root printed 0 "(pass)" lines — I didn't check why — but the same files pass when run from Software/so101. Bun also crashed on its own in an earlier run before I fixed a bad edit; the later runs were clean.
/admintest (src/admin-relay.test.ts): a stub arm server records what the relay posts.
- A uhubctl light-switch form posts the switch, and the page after the redirect shows the lightTried flash.
- A light-confirm yes form posts exactly {confirm: true}, and the page shows the lightSaved flash.
- A guest and a moderator posting {confirm: true} to /grow/light get 404, and the arm server receives nothing. This is the relay route the terminal's /grow confirm yes uses, so I tested the gate there rather than through the terminal client itself.
trial()(src/light-switch.ts):set(false)is now inside the try, and the port is always put back torestoreOnafterwards. If the cut fails and the restore fails too, the error reports both. If the restore alone fails, it says the port could not be put back. A camera-drop error is no longer hidden when the restore throws. New tests cover a failed cut followed by a restore, and the case where both fail.- README: it now says a terminal confirm is not pushed to a card that is already open, so a viewer sees it on the next load. It also says there is one waiting try-out per device, and that a second uhubctl setup replaces the first.
- Which switch a yes applies to: I added no new UI. The card and the terminal's
/growalready name the pending switch (for example "USB hub 1-1, port 2"), and the README says so. Two owners can still overwrite each other's try-out. The pending slot stays single because the review called that acceptable, and the card's text names the switch so the owner can see what a yes keeps.
🤖 Generated with Claude Code
Approved. This is the third review, so the one thing left is filed as #637.
What I checked at 07ff7dc:
- Security.
{confirm}goes through the same grow-key-gatedPOST /grow/<device>as the other light writes. The relay answers a guest or a moderator with 404 and sends nothing to the arm (tested, and I tried it on the preview:/grow/lightand/adminboth give 404 to a guest). The body goes through the existing byte-cappedreadBody. Confirm mixed with other fields gets a 400.pendingis trimmed to 80 characters inreadLight, escaped on the card and passed throughplain()in the terminal. No shell is built from user text: uhubctl and lsusb get fixed argv, and hub and port are validated byparseDriver. - Safety of the actuation.
set(false)is now inside the try. The port is always put back to what the schedule wants, so a dark period stays dark. A failed restore reports both errors. Every set re-verifies that no camera shares the port. A switch is kept only on the owner's yes. Each of these has a test. - Both doors.
/grow [light|pump] confirm yes|nois in tty, HELP, usage and the README./growprints the waiting try-out. - Tests. The light, light-endpoint, admin-relay and terminal suites pass at the head (95 pass, 0 fail). The type check is clean in the touched files.
Left for #637: the relay's light write times out at 2.5 s, and a real try-out takes more than 2 s plus several uhubctl and lsusb calls. On the Pi, the page will flash "the arm server did not answer" while the try-out actually succeeds, and the pending question only shows on reload. The admin test's stub arm answers instantly, so it can't catch this.
I could not open the forge's screenshots from this sandbox. The preview has no arm, so the try-out itself can't be seen there.
mainis already merged intosmith/582-grow-light-schedule-a-switch-driver-smar, so I have nothing new to push. I did not commit the merge myself.I resolved the conflicts in
Software/so101/src/light-endpoint.test.tsandserver.tsby takingmain's side. It is a superset of ours: it adds the grow key header to the tests and theArmAbsentError503 answer inserver.ts. After that,src/light-endpoint.test.ts,src/light.test.tsandsrc/server*passed (99 tests). I did not get a clean type check, becausebun x tsc --noEmitcouldn't find thebuntype definitions. A fullbun testrun showed 64 failures. I attribute them to two test runs going at once and did not confirm that.When I went to commit, the working tree was already clean and the branch was at
dbe4207, which matchesorigin.origin/mainis an ancestor of it, there are no conflict markers inSoftware/so101/src, andgit pushreported everything up to date. I don't know why my uncommitted merge state was gone.I did not read what
dbe4207changed in those two files, so the pull may not match my resolution.Continues #631