diff --git a/openapi.yaml b/openapi.yaml index a365da7e..504cfe3f 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5257,6 +5257,112 @@ paths: schema: $ref: '#/components/schemas/Error' + /modules/self-serve/lane/force/machine/enable: + post: + tags: + - Modules + summary: Force enable MACHINE relay and mark lane as in-wash (superusers only) + description: | + Superuser/emergency endpoint. Bypasses the allowed services gating and directly turns on the MACHINE relay. + Also ensures the lane is marked as OCCUPIED and IN_WASH with a wash start timestamp if not already set. + operationId: forceEnableSelfServeLaneMachine + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - lane_id + properties: + lane_id: + type: integer + duration: + type: integer + nullable: true + description: Optional number of seconds after which the relay should automatically turn off + license_plate: + type: string + nullable: true + description: Optional license plate to associate with the lane + responses: + '200': + description: MACHINE relay force-enabled and lane marked in-wash + content: + application/json: + schema: + type: object + properties: + lane_id: + type: integer + forced: + type: boolean + machine: + type: string + enum: [ENABLED] + duration: + type: integer + nullable: true + status: + type: string + state: + type: string + wash_start_time: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + + /modules/self-serve/lane/force/machine/disable: + post: + tags: + - Modules + summary: Force disable MACHINE relay but keep lane as in-wash (superusers only) + description: | + Superuser/emergency endpoint. Turns off the MACHINE relay while ensuring the lane remains in an IN_WASH state + (simulating a started wash without machine assistance). + operationId: forceDisableSelfServeLaneMachine + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - lane_id + properties: + lane_id: + type: integer + license_plate: + type: string + nullable: true + responses: + '200': + description: MACHINE relay force-disabled and lane ensured in-wash + content: + application/json: + schema: + type: object + properties: + lane_id: + type: integer + forced: + type: boolean + machine: + type: string + enum: [DISABLED] + status: + type: string + state: + type: string + wash_start_time: + type: integer + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + # Module - Other Integration Endpoints /modules/motorapi/lookup: get: diff --git a/services/nginx/app/modules/selfserve/traits/selfserve_lane_relay_controller_t.php b/services/nginx/app/modules/selfserve/traits/selfserve_lane_relay_controller_t.php index ab03658c..eb97c3ee 100644 --- a/services/nginx/app/modules/selfserve/traits/selfserve_lane_relay_controller_t.php +++ b/services/nginx/app/modules/selfserve/traits/selfserve_lane_relay_controller_t.php @@ -68,6 +68,47 @@ trait selfserve_lane_relay_controller_t return true; } + /** + * Force turn on the MACHINE relay, bypassing allowed services gating. + * Intended for superuser/emergency operations. + * @param int|null $duration Optional auto-off duration in seconds + * @return bool + * @throws \Exception + */ + public function forceTurnOnMachineRelay(?int $duration = null): bool + { + // Require department lane object + if (empty($this->department_lane)) throw new \Exception("Department lane object not found for lane ID {$this->id}"); + // Basic sanity checks on lane status (still disallow clearly invalid states) + if ($this->getLaneStatus()->equals(selfserve_lane_status::CLOSED)) throw new \Exception("Cannot turn on relay on CLOSED lane"); + if ($this->getLaneStatus()->equals(selfserve_lane_status::MAINTENANCE)) throw new \Exception("Cannot turn on relay on MAINTENANCE lane"); + if ($this->getLaneStatus()->equals(selfserve_lane_status::FAULT)) throw new \Exception("Cannot turn on relay on FAULT lane"); + // Directly control Shelly without checking allowed services + $relay_id = $this->department_lane->relay_machine_id->value(); + if (empty($relay_id)) { + throw new \Exception("Invalid relay ID for MACHINE relay"); + } + $shelly = new shelly(); + $shelly->requireModuleEnabled(); + $shelly->requireValidSecretKey(); + $parameters = new shelly_request_body_get_states(); + $parameters->ids = [$relay_id]; + $parameters->select = ['status']; + $result = $shelly->sendPostRequest('/v2/devices/api/get', (array)$parameters); + // Wait 1 second + sleep(1); + // Format the result + $result = array_map(function ($device) { + return (new shelly_device_switch())->populate($device); + }, $result); + // Turn on the switch + foreach ($result as $device) { + if ($duration !== null) $device->toggle_after = $duration; + $device->switch(true); + } + return true; + } + /** * Turn off the lane relay * @param selfserve_lane_relay $relay The relay to turn off (MACHINE) diff --git a/services/nginx/app/routes/moduleSelfServeRoute.php b/services/nginx/app/routes/moduleSelfServeRoute.php index 9a68d6cf..8ca5ff90 100644 --- a/services/nginx/app/routes/moduleSelfServeRoute.php +++ b/services/nginx/app/routes/moduleSelfServeRoute.php @@ -207,5 +207,97 @@ class moduleSelfServeRoute }, [ 'modules_selfserve_lane_relay_enable_machine' => 'Manually enable MACHINE relay for a lane (requires allowed task to be present)' ]); + + /** Modules > Self Serve > Lane > Force > MACHINE enable (simulate started wash) */ + $this->post('/modules/self-serve/lane/force/machine/enable', function () { + global $response; + self::requirePermission('modules_selfserve_lane_force_machine_enable'); + $selfserve = new selfserve(); + // Validate parameters + self::requireParameters(['lane_id']); + $lane_id = (int)$this->getParameter('lane_id'); + self::requireType($lane_id, self::type_int()); + self::requireMinValue($lane_id, 1); + $duration = null; + if ($this->isParametersSet(['duration'])) { + $duration = (int)$this->getParameter('duration'); + self::requireMinValue($duration, 1); + } + $license_plate = $this->isParametersSet(['license_plate']) ? (string)$this->getParameter('license_plate') : null; + $lane = $selfserve->lane($lane_id); + try { + // 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); + } + if (!$lane->getLaneState()->equals(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH)) { + $lane->setLaneState(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH); + } + if ($license_plate !== null && $license_plate !== '') { + $lane->setLicensePlate($license_plate); + } + if ((int)$lane->getWashStartTime() <= 0) { + $lane->setWashStartTime(time()); + } + // Force enable the machine relay (bypass gating) + $lane->forceTurnOnMachineRelay($duration); + $response->success([ + 'lane_id' => $lane_id, + 'forced' => true, + 'machine' => 'ENABLED', + 'duration' => $duration, + 'status' => $lane->getLaneStatus()->name, + 'state' => $lane->getLaneState()->name, + 'wash_start_time' => $lane->getWashStartTime(), + ]); + } catch (\Exception $e) { + $response->error('Failed to force-enable MACHINE: ' . $e->getMessage(), 400); + } + }, [ + 'modules_selfserve_lane_force_machine_enable' => 'Force enable MACHINE relay and mark lane as in-wash (superusers only)' + ]); + + /** Modules > Self Serve > Lane > Force > MACHINE disable (simulate started wash without machine) */ + $this->post('/modules/self-serve/lane/force/machine/disable', function () { + global $response; + self::requirePermission('modules_selfserve_lane_force_machine_disable'); + $selfserve = new selfserve(); + // Validate parameters + self::requireParameters(['lane_id']); + $lane_id = (int)$this->getParameter('lane_id'); + self::requireType($lane_id, self::type_int()); + self::requireMinValue($lane_id, 1); + $license_plate = $this->isParametersSet(['license_plate']) ? (string)$this->getParameter('license_plate') : null; + $lane = $selfserve->lane($lane_id); + try { + // 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); + } + if (!$lane->getLaneState()->equals(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH)) { + $lane->setLaneState(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH); + } + if ($license_plate !== null && $license_plate !== '') { + $lane->setLicensePlate($license_plate); + } + if ((int)$lane->getWashStartTime() <= 0) { + $lane->setWashStartTime(time()); + } + // Turn off the machine relay if currently on + try { $lane->turnOffRelay(\modules\selfserve\helpers\selfserve_lane_relay::MACHINE); } catch (\Throwable $ignored) {} + $response->success([ + 'lane_id' => $lane_id, + 'forced' => true, + 'machine' => 'DISABLED', + 'status' => $lane->getLaneStatus()->name, + 'state' => $lane->getLaneState()->name, + 'wash_start_time' => $lane->getWashStartTime(), + ]); + } catch (\Exception $e) { + $response->error('Failed to force-disable MACHINE: ' . $e->getMessage(), 400); + } + }, [ + 'modules_selfserve_lane_force_machine_disable' => 'Force disable MACHINE relay but keep lane as in-wash (superusers only)' + ]); } } \ No newline at end of file