Add unit tests and route updates for department self-serve enabled flag relay synchronization. Refactor relay handling with hard set methods and lane status guard bypass.

This commit is contained in:
Jeppe Bundgaard
2026-03-25 14:24:30 +01:00
parent 2a9ed5af0f
commit 416eba7de5
4 changed files with 150 additions and 9 deletions
@@ -83,6 +83,16 @@ trait selfserve_lane_relay_controller_t
return $this->setRelayStatus(selfserve_lane_relay::MACHINE, $on);
}
/**
* Set MACHINE relay status directly, bypassing lane status guards.
* Intended for department-level operational toggles.
* @throws \Exception
*/
public function setMachineRelayStatusHard(bool $on): bool
{
return $this->setRelayStatusHard(selfserve_lane_relay::MACHINE, $on);
}
/**
* Set MACHINE_PROGRAM_PICKER relay status directly.
* @param bool $on true to turn on, false to turn off
@@ -94,6 +104,16 @@ trait selfserve_lane_relay_controller_t
return $this->setRelayStatus(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, $on);
}
/**
* Set MACHINE_PROGRAM_PICKER relay status directly, bypassing lane status guards.
* Intended for department-level operational toggles.
* @throws \Exception
*/
public function setMachineProgramPickerRelayStatusHard(bool $on): bool
{
return $this->setRelayStatusHard(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, $on);
}
/**
* Set MACHINE_CLEANER relay status directly.
* @param bool $on true to turn on, false to turn off
@@ -105,6 +125,16 @@ trait selfserve_lane_relay_controller_t
return $this->setRelayStatus(selfserve_lane_relay::MACHINE_CLEANER, $on);
}
/**
* Set MACHINE_CLEANER relay status directly, bypassing lane status guards.
* Intended for department-level operational toggles.
* @throws \Exception
*/
public function setMachineCleanerRelayStatusHard(bool $on): bool
{
return $this->setRelayStatusHard(selfserve_lane_relay::MACHINE_CLEANER, $on);
}
/**
* Set relay status directly for a specific relay type.
* @param selfserve_lane_relay $relay
@@ -119,6 +149,15 @@ trait selfserve_lane_relay_controller_t
: $this->forceTurnOffRelay($relay);
}
/**
* Set relay status directly for a specific relay type, bypassing lane status guards.
* @throws \Exception
*/
public function setRelayStatusHard(selfserve_lane_relay $relay, bool $on): bool
{
return $this->sendRelaySwitchCommand($relay, $on);
}
/**
* Resolve relay ID for the current lane.
* @param selfserve_lane_relay $relay
@@ -677,14 +716,9 @@ trait selfserve_lane_relay_controller_t
'id' => $relay_id,
'channel' => self::SHELLY_DEFAULT_CHANNEL,
'on' => $on,
// We keep toggle_after=0 to avoid unintended auto toggle behavior.
'toggle_after' => 0,
];
if ($duration !== null && $duration > 0) {
// Current relay API behavior intentionally keeps manual relay commands explicit;
// duration is accepted by route contracts but does not auto-toggle at Shelly level.
$payload['toggle_after'] = 0;
}
// Keep duration parameter for route compatibility, but do not pass toggle_after.
// Some Shelly firmware variants interpret toggle_after=0 as immediate toggle.
$response = $this->sendShellyPostRateLimited('/v2/devices/api/set/switch', $payload);
$this->seedLaneShellySnapshotFromSwitch($relay_id, $on, $response);
@@ -737,4 +771,3 @@ trait selfserve_lane_relay_controller_t
return $devices[0];
}
}
+71 -1
View File
@@ -3,8 +3,10 @@
namespace routes;
use classes\authentication;
use classes\selfserve;
use objects\categories_o;
use objects\department_categories_o;
use objects\department_lanes_o;
use objects\department_variables_o;
use objects\departments_o;
use objects\logs_o;
@@ -287,6 +289,7 @@ class departmentsRoute
$department_variables = (new department_variables_o())->selectDepartment($department->id);
$enabled = self::getParameter('enabled') === 'true' || self::getParameter('enabled') === true || self::getParameter('enabled') === 1 || self::getParameter('enabled') === '1';
$department_variables->set('selfserve_enabled', $enabled ? 'true' : 'false');
$this->syncDepartmentSelfServeRelayStates((int)$department->id, $enabled);
// Log the incident
(new logs_o())->add('departments', $department->id, 1, $user->id, 'EDIT_DEPARTMENT_SELFSERVE_ENABLED', 'Successfully edited department self-serve enabled status to ' . ($enabled ? 'true' : 'false'));
@@ -490,4 +493,71 @@ class departmentsRoute
]
);
}
}
protected function syncDepartmentSelfServeRelayStates(int $departmentId, bool $enabled): void
{
$selfserve = new selfserve();
$lanes = (new department_lanes_o())->getDepartmentLanes($departmentId);
foreach ($lanes as $department_lane) {
$lane_id = (int)$department_lane->id;
if ($lane_id <= 0) {
continue;
}
try {
$lane = $selfserve->lane($lane_id);
} catch (\Throwable) {
continue;
}
if ($enabled) {
// Self-serve enabled: keep machine stack off.
$this->setOptionalLaneRelayState($lane, 'relay_machine_program_picker_id', static function () use ($lane): void {
$lane->setMachineProgramPickerRelayStatusHard(false);
});
$this->setOptionalLaneRelayState($lane, 'relay_machine_cleaner_id', static function () use ($lane): void {
$lane->setMachineCleanerRelayStatusHard(false);
});
try {
$lane->setMachineRelayStatusHard(false);
} catch (\Throwable) {}
continue;
}
// Self-serve disabled: restore machine stack on.
$this->setOptionalLaneRelayState($lane, 'relay_machine_cleaner_id', static function () use ($lane): void {
$lane->setMachineCleanerRelayStatusHard(true);
});
try {
$lane->setMachineRelayStatusHard(true);
} catch (\Throwable) {}
$this->setOptionalLaneRelayState($lane, 'relay_machine_program_picker_id', static function () use ($lane): void {
$lane->setMachineProgramPickerRelayStatusHard(true);
});
}
}
protected function setOptionalLaneRelayState(object $lane, string $relayProperty, callable $callback): void
{
if (
empty($lane->department_lane)
|| !isset($lane->department_lane->{$relayProperty})
|| !is_object($lane->department_lane->{$relayProperty})
|| !method_exists($lane->department_lane->{$relayProperty}, 'value')
) {
return;
}
$relay_id = trim((string)$lane->department_lane->{$relayProperty}->value());
if ($relay_id === '') {
return;
}
try {
$callback();
} catch (\Throwable) {
// Best effort only; this endpoint should still update the department variable.
}
}
}
@@ -0,0 +1,15 @@
<?php
it('syncs lane relay states when department self-serve enabled flag changes', function (): void {
$routeContent = file_get_contents(app_path('routes/departmentsRoute.php'));
expect($routeContent)->not->toBeFalse();
expect($routeContent)->toContain('/departments/self-serve/enabled');
expect($routeContent)->toContain('$this->syncDepartmentSelfServeRelayStates((int)$department->id, $enabled);');
expect($routeContent)->toContain('setMachineProgramPickerRelayStatusHard(false)');
expect($routeContent)->toContain('setMachineCleanerRelayStatusHard(false)');
expect($routeContent)->toContain('setMachineRelayStatusHard(false)');
expect($routeContent)->toContain('setMachineCleanerRelayStatusHard(true)');
expect($routeContent)->toContain('setMachineRelayStatusHard(true)');
expect($routeContent)->toContain('setMachineProgramPickerRelayStatusHard(true)');
});
@@ -378,4 +378,27 @@ it('uses direct set/switch and seeds cache so immediate status read does not cal
expect($harness->getShellyCallCount('/v2/devices/api/set/switch'))->toBe(1);
expect($harness->getShellyCallCount('/v2/devices/api/get'))->toBe(0);
expect($harness->shellyCalls[0]['endpoint'])->toBe('/v2/devices/api/set/switch');
expect($harness->shellyCalls[0]['payload'])->toMatchArray([
'id' => 'relay-program',
'channel' => 0,
'on' => true,
]);
expect(array_key_exists('toggle_after', $harness->shellyCalls[0]['payload']))->toBeFalse();
});
it('supports hard relay set even when lane status is CLOSED', function (): void {
$harness = selfserve_lane_shelly_test_harness();
$harness->setLaneStatus(selfserve_lane_status::CLOSED);
$harness->queueShellyResponse('/v2/devices/api/set/switch', [
[
'id' => 'relay-machine',
'online' => true,
'status' => ['switch:0' => ['output' => false]],
],
]);
$result = $harness->setMachineRelayStatusHard(false);
expect($result)->toBeTrue();
expect($harness->getShellyCallCount('/v2/devices/api/set/switch'))->toBe(1);
});