From 6d09e5449e47c06c79bc37eca96f0a4eb6d5a5ca Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Thu, 19 Feb 2026 11:37:16 +0100 Subject: [PATCH] Enhance MACHINE relay control logic and add emergency override functionality - Add `forceTurnOffMachineRelay` method for superuser/emergency operations to bypass gating restrictions. - Enforce `toggle_after = 0` to prevent auto-toggle behavior. - Update routes to explicitly reflect relay state when modifying MACHINE relay. - Extend Shelly device switch handling with optional `skip_toggle_after` parameter. --- .../helpers/selfserve_lane_state.php | 5 +++ .../selfserve_lane_relay_controller_t.php | 45 ++++++++++++++++++- .../shelly/helpers/shelly_device_switch.php | 5 ++- .../nginx/app/routes/moduleSelfServeRoute.php | 8 +++- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/services/nginx/app/modules/selfserve/helpers/selfserve_lane_state.php b/services/nginx/app/modules/selfserve/helpers/selfserve_lane_state.php index d116171d..cffc5170 100644 --- a/services/nginx/app/modules/selfserve/helpers/selfserve_lane_state.php +++ b/services/nginx/app/modules/selfserve/helpers/selfserve_lane_state.php @@ -17,4 +17,9 @@ enum selfserve_lane_state case FAULT; // The lane is in a fault state case MAINTENANCE; // The lane is under maintenance case CLOSED; // The lane is closed + + public function equals(selfserve_lane_state $state): bool + { + return $this === $state; + } } 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 eb97c3ee..439c3450 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 @@ -62,7 +62,8 @@ trait selfserve_lane_relay_controller_t }, $result); // Turn on the switch foreach ($result as $device) { - if ($duration !== null) $device->toggle_after = $duration; // Set the duration to turn off the relay (if specified) + // Ensure machine switches never auto-toggle off; enforce toggle_after = 0 + $device->toggle_after = 0; $device->switch(true); } return true; @@ -103,12 +104,52 @@ trait selfserve_lane_relay_controller_t }, $result); // Turn on the switch foreach ($result as $device) { - if ($duration !== null) $device->toggle_after = $duration; + // Ensure machine switches never auto-toggle off; enforce toggle_after = 0 + $device->toggle_after = 0; $device->switch(true); } return true; } + /** + * Force turn off the MACHINE relay, bypassing allowed services gating. + * Intended for superuser/emergency operations. + * @return bool + * @throws \Exception + */ + public function forceTurnOffMachineRelay(): 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 off relay on CLOSED lane"); + if ($this->getLaneStatus()->equals(selfserve_lane_status::MAINTENANCE)) throw new \Exception("Cannot turn off relay on MAINTENANCE lane"); + if ($this->getLaneStatus()->equals(selfserve_lane_status::FAULT)) throw new \Exception("Cannot turn off 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 off the switch + foreach ($result as $device) { + $device->switch(false); + } + return true; + } + /** * Turn off the lane relay * @param selfserve_lane_relay $relay The relay to turn off (MACHINE) diff --git a/services/nginx/app/modules/shelly/helpers/shelly_device_switch.php b/services/nginx/app/modules/shelly/helpers/shelly_device_switch.php index e5913ed8..d16287c3 100644 --- a/services/nginx/app/modules/shelly/helpers/shelly_device_switch.php +++ b/services/nginx/app/modules/shelly/helpers/shelly_device_switch.php @@ -35,16 +35,17 @@ class shelly_device_switch extends shelly_device_state /** * Send a request to the shelly API to switch the output * @param bool $on + * @param bool|null $skip_toggle_after Whether to prevent the toggle_after parameter from being sent (if null, will use the value of $this->toggle_after) * @return array|object|null * @throws Exception */ - public function switch(bool $on): array|object|null + public function switch(bool $on, ?bool $skip_toggle_after = false): array|object|null { $parameters = [ 'id' => (string)$this->id, 'channel' => (int)$this->channel, 'on' => $on, - 'toggle_after' => (int)$this->toggle_after, + 'toggle_after' => $skip_toggle_after ? 0 : (int)$this->toggle_after, ]; return (new shelly())->sendPostRequest('/v2/devices/api/set/switch', $parameters); } diff --git a/services/nginx/app/routes/moduleSelfServeRoute.php b/services/nginx/app/routes/moduleSelfServeRoute.php index 8ca5ff90..f8ab16d1 100644 --- a/services/nginx/app/routes/moduleSelfServeRoute.php +++ b/services/nginx/app/routes/moduleSelfServeRoute.php @@ -241,6 +241,8 @@ class moduleSelfServeRoute } // Force enable the machine relay (bypass gating) $lane->forceTurnOnMachineRelay($duration); + // Reflect relay state explicitly + try { $lane->setLaneState(\modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_ON); } catch (\Throwable $ignored) {} $response->success([ 'lane_id' => $lane_id, 'forced' => true, @@ -283,8 +285,10 @@ class moduleSelfServeRoute 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) {} + // Turn off the machine relay (do not swallow errors) + $lane->forceTurnOffMachineRelay(); + // Reflect relay state explicitly + try { $lane->setLaneState(\modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_OFF); } catch (\Throwable $ignored) {} $response->success([ 'lane_id' => $lane_id, 'forced' => true,