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:
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user