Handle relay command job timeouts for edge gateways

- Introduce `expireTimedOutRelayStatusCommandJobs` to clean up long-pending relay status command jobs.
- Add `TIMED_OUT` status for relay command jobs and incorporate it into job status evaluations.
- Refactor command job finalization to support timeout-specific error messaging.
- Improve handling of fast-path failures in edge broker commands.
This commit is contained in:
Jeppe Bundgaard
2026-04-27 16:47:09 +02:00
parent 86eec9a51e
commit feb9aac4f7
16 changed files with 636 additions and 108 deletions
@@ -323,6 +323,7 @@ class moduleSelfServeRoute
}
// Execute the command
try {
$this->applyShellyTransportOverride($lane);
$args = new \modules\selfserve\classes\selfserve_lane_command_arguments();
$args->setParameters([
...$this->getParametersAsArray(), // Pass all parameters
@@ -338,6 +339,7 @@ class moduleSelfServeRoute
'elapsed_wash_time' => $lane->getElapsedWashTime(),
'license_plate' => $lane->getLicensePlate(),
'customer_number' => $lane->getCustomerNumber(),
'transport' => $this->requestedShellyTransportOverride(),
]);
} catch (\Exception $e) {
if (
@@ -409,12 +411,18 @@ class moduleSelfServeRoute
}
}
// Persist on lane cache (overwrites previous allowed services)
$relay_sync = $lane->syncMachineRelayFromVisibleServices($allowed_services, true);
$response->success([
'lane_id' => $lane_id,
'allowed_services' => $allowed_services,
'relay_sync' => $relay_sync,
]);
try {
$this->applyShellyTransportOverride($lane);
$relay_sync = $lane->syncMachineRelayFromVisibleServices($allowed_services, true);
$response->success([
'lane_id' => $lane_id,
'allowed_services' => $allowed_services,
'relay_sync' => $relay_sync,
'transport' => $this->requestedShellyTransportOverride(),
]);
} catch (\Exception $e) {
$response->error('Failed to set allowed services: ' . $e->getMessage(), 400);
}
}, [
'modules_selfserve_lane_services_set_allowed' => 'Set allowed services for a lane based on currently shown tasks (post-Q&A)'
]);
@@ -472,14 +480,7 @@ class moduleSelfServeRoute
try {
$this->applyShellyTransportOverride($lane);
$status = $lane->getMachineProgramPickerRelayStatus();
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE_PROGRAM_PICKER',
'relay_id' => (string)$status['relay_id'],
'online' => (bool)$status['online'],
'on' => (bool)$status['on'],
'transport' => $this->requestedShellyTransportOverride(),
]);
$response->success($this->buildRelayStatusResponse($lane_id, 'MACHINE_PROGRAM_PICKER', $status));
} catch (\Exception $e) {
$response->error('Failed to get MACHINE_PROGRAM_PICKER relay status: ' . $e->getMessage(), 400);
}
@@ -508,15 +509,9 @@ class moduleSelfServeRoute
$this->applyShellyTransportOverride($lane);
$lane->setMachineProgramPickerRelayStatus((bool)$on);
$status = $lane->getMachineProgramPickerRelayStatus();
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE_PROGRAM_PICKER',
$response->success($this->buildRelayStatusResponse($lane_id, 'MACHINE_PROGRAM_PICKER', $status, [
'requested_on' => (bool)$on,
'relay_id' => (string)$status['relay_id'],
'online' => (bool)$status['online'],
'on' => (bool)$status['on'],
'transport' => $this->requestedShellyTransportOverride(),
]);
]));
} catch (\Exception $e) {
$response->error('Failed to set MACHINE_PROGRAM_PICKER relay status: ' . $e->getMessage(), 400);
}
@@ -538,14 +533,7 @@ class moduleSelfServeRoute
try {
$this->applyShellyTransportOverride($lane);
$status = $lane->getMachineCleanerRelayStatus();
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE_CLEANER',
'relay_id' => (string)$status['relay_id'],
'online' => (bool)$status['online'],
'on' => (bool)$status['on'],
'transport' => $this->requestedShellyTransportOverride(),
]);
$response->success($this->buildRelayStatusResponse($lane_id, 'MACHINE_CLEANER', $status));
} catch (\Exception $e) {
$response->error('Failed to get MACHINE_CLEANER relay status: ' . $e->getMessage(), 400);
}
@@ -574,15 +562,9 @@ class moduleSelfServeRoute
$this->applyShellyTransportOverride($lane);
$lane->setMachineCleanerRelayStatus((bool)$on);
$status = $lane->getMachineCleanerRelayStatus();
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE_CLEANER',
$response->success($this->buildRelayStatusResponse($lane_id, 'MACHINE_CLEANER', $status, [
'requested_on' => (bool)$on,
'relay_id' => (string)$status['relay_id'],
'online' => (bool)$status['online'],
'on' => (bool)$status['on'],
'transport' => $this->requestedShellyTransportOverride(),
]);
]));
} catch (\Exception $e) {
$response->error('Failed to set MACHINE_CLEANER relay status: ' . $e->getMessage(), 400);
}
@@ -604,14 +586,7 @@ class moduleSelfServeRoute
try {
$this->applyShellyTransportOverride($lane);
$status = $lane->getMachineRelayStatus();
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE',
'relay_id' => (string)$status['relay_id'],
'online' => (bool)$status['online'],
'on' => (bool)$status['on'],
'transport' => $this->requestedShellyTransportOverride(),
]);
$response->success($this->buildRelayStatusResponse($lane_id, 'MACHINE', $status));
} catch (\Exception $e) {
$response->error('Failed to get MACHINE relay status: ' . $e->getMessage(), 400);
}
@@ -657,15 +632,9 @@ class moduleSelfServeRoute
} catch (\Throwable $ignored) {}
$status = $lane->getMachineRelayStatus();
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE',
$response->success($this->buildRelayStatusResponse($lane_id, 'MACHINE', $status, [
'requested_on' => (bool)$on,
'relay_id' => (string)$status['relay_id'],
'online' => (bool)$status['online'],
'on' => (bool)$status['on'],
'transport' => $this->requestedShellyTransportOverride(),
]);
]));
} catch (\Exception $e) {
$response->error('Failed to set MACHINE relay status: ' . $e->getMessage(), 400);
}
@@ -690,8 +659,15 @@ class moduleSelfServeRoute
}
$lane = $selfserve->lane($lane_id);
try {
$lane->turnOnRelay(selfserve_lane_relay::MACHINE_PROGRAM_PICKER);
$response->success(['lane_id' => $lane_id, 'relay' => 'MACHINE_PROGRAM_PICKER', 'enabled' => true, 'duration' => $duration]);
$this->applyShellyTransportOverride($lane);
$lane->turnOnRelay(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, $duration);
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE_PROGRAM_PICKER',
'enabled' => true,
'duration' => $duration,
'transport' => $this->requestedShellyTransportOverride(),
]);
} catch (\Exception $e) {
$response->error('Failed to enable MACHINE_PROGRAM_PICKER relay: ' . $e->getMessage(), 400);
}
@@ -716,8 +692,15 @@ class moduleSelfServeRoute
}
$lane = $selfserve->lane($lane_id);
try {
$lane->turnOnRelay(selfserve_lane_relay::MACHINE_CLEANER);
$response->success(['lane_id' => $lane_id, 'relay' => 'MACHINE_CLEANER', 'enabled' => true, 'duration' => $duration]);
$this->applyShellyTransportOverride($lane);
$lane->turnOnRelay(selfserve_lane_relay::MACHINE_CLEANER, $duration);
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE_CLEANER',
'enabled' => true,
'duration' => $duration,
'transport' => $this->requestedShellyTransportOverride(),
]);
} catch (\Exception $e) {
$response->error('Failed to enable MACHINE_CLEANER relay: ' . $e->getMessage(), 400);
}
@@ -742,7 +725,8 @@ class moduleSelfServeRoute
}
$lane = $selfserve->lane($lane_id);
try {
$lane->turnOnRelay(selfserve_lane_relay::MACHINE);
$this->applyShellyTransportOverride($lane);
$lane->turnOnRelay(selfserve_lane_relay::MACHINE, $duration);
// A started wash should also turn on cleaner when configured.
try {
if (
@@ -753,7 +737,13 @@ class moduleSelfServeRoute
$lane->setMachineCleanerRelayStatusHard(true);
}
} catch (\Throwable $ignored) {}
$response->success(['lane_id' => $lane_id, 'relay' => 'MACHINE', 'enabled' => true, 'duration' => $duration]);
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE',
'enabled' => true,
'duration' => $duration,
'transport' => $this->requestedShellyTransportOverride(),
]);
} catch (\Exception $e) {
$msg = $e->getMessage();
// If gating prevented activation, respond with 403
@@ -783,8 +773,15 @@ class moduleSelfServeRoute
}
$lane = $selfserve->lane($lane_id);
try {
$this->applyShellyTransportOverride($lane);
$lane->forceTurnOnRelay(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, $duration);
$response->success(['lane_id' => $lane_id, 'relay' => 'MACHINE_PROGRAM_PICKER', 'enabled' => true, 'duration' => $duration]);
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE_PROGRAM_PICKER',
'enabled' => true,
'duration' => $duration,
'transport' => $this->requestedShellyTransportOverride(),
]);
} catch (\Exception $e) {
$response->error('Failed to force enable MACHINE_PROGRAM_PICKER relay: ' . $e->getMessage(), 400);
}
@@ -804,8 +801,14 @@ class moduleSelfServeRoute
self::requireMinValue($lane_id, 1);
$lane = $selfserve->lane($lane_id);
try {
$this->applyShellyTransportOverride($lane);
$lane->forceTurnOffRelay(selfserve_lane_relay::MACHINE_PROGRAM_PICKER);
$response->success(['lane_id' => $lane_id, 'relay' => 'MACHINE_PROGRAM_PICKER', 'disabled' => true]);
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE_PROGRAM_PICKER',
'disabled' => true,
'transport' => $this->requestedShellyTransportOverride(),
]);
} catch (\Exception $e) {
$response->error('Failed to force disable MACHINE_PROGRAM_PICKER relay: ' . $e->getMessage(), 400);
}
@@ -830,8 +833,15 @@ class moduleSelfServeRoute
}
$lane = $selfserve->lane($lane_id);
try {
$this->applyShellyTransportOverride($lane);
$lane->forceTurnOnRelay(selfserve_lane_relay::MACHINE_CLEANER, $duration);
$response->success(['lane_id' => $lane_id, 'relay' => 'MACHINE_CLEANER', 'enabled' => true, 'duration' => $duration]);
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE_CLEANER',
'enabled' => true,
'duration' => $duration,
'transport' => $this->requestedShellyTransportOverride(),
]);
} catch (\Exception $e) {
$response->error('Failed to force enable MACHINE_CLEANER relay: ' . $e->getMessage(), 400);
}
@@ -851,8 +861,14 @@ class moduleSelfServeRoute
self::requireMinValue($lane_id, 1);
$lane = $selfserve->lane($lane_id);
try {
$this->applyShellyTransportOverride($lane);
$lane->forceTurnOffRelay(selfserve_lane_relay::MACHINE_CLEANER);
$response->success(['lane_id' => $lane_id, 'relay' => 'MACHINE_CLEANER', 'disabled' => true]);
$response->success([
'lane_id' => $lane_id,
'relay' => 'MACHINE_CLEANER',
'disabled' => true,
'transport' => $this->requestedShellyTransportOverride(),
]);
} catch (\Exception $e) {
$response->error('Failed to force disable MACHINE_CLEANER relay: ' . $e->getMessage(), 400);
}
@@ -878,6 +894,7 @@ class moduleSelfServeRoute
$license_plate = $this->isParametersSet(['license_plate']) ? (string)$this->getParameter('license_plate') : null;
$lane = $selfserve->lane($lane_id);
try {
$this->applyShellyTransportOverride($lane);
// If lane looks idle/available, mark as occupied and in wash, set timers
if ($lane->getLaneStatus()->equals(\modules\selfserve\helpers\selfserve_lane_status::AVAILABLE)) {
$lane->setLaneStatus(\modules\selfserve\helpers\selfserve_lane_status::OCCUPIED);
@@ -924,6 +941,7 @@ class moduleSelfServeRoute
'status' => $lane->getLaneStatus()->name,
'state' => $lane->getLaneState()->name,
'wash_start_time' => $lane->getWashStartTime(),
'transport' => $this->requestedShellyTransportOverride(),
]);
} catch (\Exception $e) {
$response->error('Failed to force-enable MACHINE: ' . $e->getMessage(), 400);
@@ -945,6 +963,7 @@ class moduleSelfServeRoute
$license_plate = $this->isParametersSet(['license_plate']) ? (string)$this->getParameter('license_plate') : null;
$lane = $selfserve->lane($lane_id);
try {
$this->applyShellyTransportOverride($lane);
// Ensure lane reflects an ongoing wash, but with machine off
if ($lane->getLaneStatus()->equals(\modules\selfserve\helpers\selfserve_lane_status::AVAILABLE)) {
$lane->setLaneStatus(\modules\selfserve\helpers\selfserve_lane_status::OCCUPIED);
@@ -990,6 +1009,7 @@ class moduleSelfServeRoute
'status' => $lane->getLaneStatus()->name,
'state' => $lane->getLaneState()->name,
'wash_start_time' => $lane->getWashStartTime(),
'transport' => $this->requestedShellyTransportOverride(),
]);
} catch (\Exception $e) {
$response->error('Failed to force-disable MACHINE: ' . $e->getMessage(), 400);
@@ -999,6 +1019,40 @@ class moduleSelfServeRoute
]);
}
/**
* @param array<string,mixed> $status
* @param array<string,mixed> $extra
* @return array<string,mixed>
*/
private function buildRelayStatusResponse(int $lane_id, string $relay, array $status, array $extra = []): array
{
$on = (bool)($status['on'] ?? false);
$switch_status = is_array($status['status'] ?? null) ? $status['status'] : [];
if (!isset($switch_status['switch:0']) || !is_array($switch_status['switch:0'])) {
$switch_status['switch:0'] = [];
}
$switch_status['switch:0']['output'] = (bool)($switch_status['switch:0']['output'] ?? $on);
$payload = [
'lane_id' => $lane_id,
'relay' => $relay,
...$extra,
'relay_id' => (string)($status['relay_id'] ?? ''),
'online' => (bool)($status['online'] ?? false),
'on' => $on,
'status' => $switch_status,
'transport' => $this->requestedShellyTransportOverride(),
];
foreach (['binding', 'execution', 'raw'] as $metadata_key) {
if (isset($status[$metadata_key]) && is_array($status[$metadata_key]) && $status[$metadata_key] !== []) {
$payload[$metadata_key] = $status[$metadata_key];
}
}
return $payload;
}
/**
* @throws \Exception
*/
@@ -1036,7 +1090,7 @@ class moduleSelfServeRoute
}
if (!in_array($normalized, ['local', 'cloud'], true)) {
throw new \InvalidArgumentException('Invalid transport. Expected local or cloud.');
throw new \InvalidArgumentException('Invalid transport. Expected local, gateway, or cloud.');
}
return $normalized;