Add unit tests and routes for department weather targets, including GET/PUT endpoints, thresholds validation, and aggregate status handling. Extend OpenAPI spec with schema mappings for department weather targets and machine relay endpoints.

This commit is contained in:
Jeppe Bundgaard
2026-03-25 11:14:25 +01:00
parent 6fdc8d466e
commit 7fc70184df
16 changed files with 1765 additions and 88 deletions
@@ -5,4 +5,6 @@ namespace modules\selfserve\helpers;
enum selfserve_lane_relay
{
case MACHINE; // Relay that controls the machine power
case MACHINE_PROGRAM_PICKER; // Relay that controls the machine program picker
case MACHINE_CLEANER; // Relay that controls the machine cleaner
}
@@ -358,6 +358,8 @@ Purpose: operational lane control and relay management.
| `GET /modules/self-serve/lane/status` | optional `lane_id`, default `1` | `modules_selfserve_lane_status_view` | Returns lane status, mode, state, wash timer, reg, and customer number. |
| `POST /modules/self-serve/lane/command` | `lane_id`, `command` | `modules_selfserve_lane_command_execute` plus command-specific permission | Valid commands: `START`, `STOP`, `RESET`, `RESERVE`, `RELEASE`. |
| `POST /modules/self-serve/lane/services/allowed` | `lane_id`, optional `task_ids` | `modules_selfserve_lane_services_set_allowed` | Writes allowed service names to the lane cache. |
| `GET /modules/self-serve/lane/relay/machine/status` | `lane_id` | `modules_selfserve_lane_relay_machine_status_view` | Reads the Shelly MACHINE relay state (`on`/`off`) for the lane. |
| `POST /modules/self-serve/lane/relay/machine/set` | `lane_id`, `on` | `modules_selfserve_lane_relay_machine_status_set` | Sets Shelly MACHINE relay state directly (`on=true/false`) and returns updated status. |
| `POST /modules/self-serve/lane/relay/machine/enable` | `lane_id`, optional `duration` | `modules_selfserve_lane_relay_enable_machine` | Manual enable, still gated by allowed services. |
| `POST /modules/self-serve/lane/force/machine/enable` | `lane_id`, optional `duration`, optional `license_plate` | `modules_selfserve_lane_force_machine_enable` | Bypasses service gating and marks the lane as in wash. |
| `POST /modules/self-serve/lane/force/machine/disable` | `lane_id`, optional `license_plate` | `modules_selfserve_lane_force_machine_disable` | Keeps the lane in wash but turns the machine relay off. |
@@ -15,9 +15,190 @@ use modules\shelly\helpers\shelly_request_body_get_states;
trait selfserve_lane_relay_controller_t
{
/**
* Get current MACHINE relay status from Shelly.
* @return array{relay_id: string, online: bool, on: bool}
* @throws \Exception
*/
public function getMachineRelayStatus(): array
{
return $this->getRelayStatus(selfserve_lane_relay::MACHINE);
}
/**
* Get current MACHINE_PROGRAM_PICKER relay status from Shelly.
* @return array{relay_id: string, online: bool, on: bool}
* @throws \Exception
*/
public function getMachineProgramPickerRelayStatus(): array
{
return $this->getRelayStatus(selfserve_lane_relay::MACHINE_PROGRAM_PICKER);
}
/**
* Get current MACHINE_CLEANER relay status from Shelly.
* @return array{relay_id: string, online: bool, on: bool}
* @throws \Exception
*/
public function getMachineCleanerRelayStatus(): array
{
return $this->getRelayStatus(selfserve_lane_relay::MACHINE_CLEANER);
}
/**
* Get current relay status from Shelly for a specific relay type.
* @param selfserve_lane_relay $relay
* @return array{relay_id: string, online: bool, on: bool}
* @throws \Exception
*/
public function getRelayStatus(selfserve_lane_relay $relay): array
{
$relay_id = $this->getRelayId($relay);
$devices = $this->fetchRelaySwitches($relay_id);
if (count($devices) < 1) {
throw new \Exception("No Shelly device state returned for {$relay->name} relay");
}
$device = $devices[0];
return [
'relay_id' => $relay_id,
'online' => isset($device->online) && (int)$device->online === 1,
'on' => $this->extractRelayOnState($device, $relay),
];
}
/**
* Set MACHINE relay status directly.
* @param bool $on true to turn on, false to turn off
* @return bool
* @throws \Exception
*/
public function setMachineRelayStatus(bool $on): bool
{
return $this->setRelayStatus(selfserve_lane_relay::MACHINE, $on);
}
/**
* Set MACHINE_PROGRAM_PICKER relay status directly.
* @param bool $on true to turn on, false to turn off
* @return bool
* @throws \Exception
*/
public function setMachineProgramPickerRelayStatus(bool $on): bool
{
return $this->setRelayStatus(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, $on);
}
/**
* Set MACHINE_CLEANER relay status directly.
* @param bool $on true to turn on, false to turn off
* @return bool
* @throws \Exception
*/
public function setMachineCleanerRelayStatus(bool $on): bool
{
return $this->setRelayStatus(selfserve_lane_relay::MACHINE_CLEANER, $on);
}
/**
* Set relay status directly for a specific relay type.
* @param selfserve_lane_relay $relay
* @param bool $on true to turn on, false to turn off
* @return bool
* @throws \Exception
*/
public function setRelayStatus(selfserve_lane_relay $relay, bool $on): bool
{
return $on
? $this->forceTurnOnRelay($relay)
: $this->forceTurnOffRelay($relay);
}
/**
* Resolve relay ID for the current lane.
* @param selfserve_lane_relay $relay
* @throws \Exception
*/
private function getRelayId(selfserve_lane_relay $relay): string
{
if (empty($this->department_lane)) {
throw new \Exception("Department lane object not found for lane ID {$this->id}");
}
$relay_id = match ($relay) {
selfserve_lane_relay::MACHINE => (string)$this->department_lane->relay_machine_id->value(),
selfserve_lane_relay::MACHINE_PROGRAM_PICKER => (string)$this->department_lane->relay_machine_program_picker_id->value(),
selfserve_lane_relay::MACHINE_CLEANER => (string)$this->department_lane->relay_machine_cleaner_id->value(),
default => throw new \Exception("Invalid relay type: {$relay->name}"),
};
if ($relay_id === '') {
throw new \Exception("Invalid relay ID for {$relay->name} relay");
}
return $relay_id;
}
/**
* Fetch Shelly switch state objects for a relay ID.
* @param string $relay_id
* @return array<shelly_device_switch>
* @throws \Exception
*/
private function fetchRelaySwitches(string $relay_id): array
{
$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);
if (is_object($result)) {
$result = [$result];
}
if (!is_array($result)) {
return [];
}
return array_map(function ($device) {
return (new shelly_device_switch())->populate($device);
}, $result);
}
/**
* Extract boolean on/off status from a Shelly switch state object.
* @throws \Exception
*/
private function extractRelayOnState(shelly_device_switch $device, selfserve_lane_relay $relay): bool
{
if (isset($device->on)) {
return (bool)$device->on;
}
if (!isset($device->status) || !is_object($device->status)) {
throw new \Exception('Missing status payload from Shelly response');
}
$status = (array)$device->status;
foreach (['switch:0', 'switch_0', 'switch0'] as $switch_key) {
if (!array_key_exists($switch_key, $status)) {
continue;
}
$switch_state = $status[$switch_key];
if (is_object($switch_state) && isset($switch_state->output)) {
return (bool)$switch_state->output;
}
if (is_array($switch_state) && array_key_exists('output', $switch_state)) {
return (bool)$switch_state['output'];
}
}
throw new \Exception("Unable to determine {$relay->name} relay state from Shelly status payload");
}
/**
* Turn on the lane relay
* @param selfserve_lane_relay $relay The relay to turn on (MACHINE)
* @param selfserve_lane_relay $relay The relay to turn on (MACHINE or MACHINE_PROGRAM_PICKER)
* @parm int|null $duration The duration in seconds to keep the relay on (optional)
* @return bool True if the relay was successfully turned on
* @throws \Exception If an invalid relay is specified or if the lane is not in a state to turn on the relay
@@ -30,7 +211,8 @@ trait selfserve_lane_relay_controller_t
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");
// Enforce that MACHINE relay can only be enabled when allowed by current self-serve tasks (self-serve, manual trigger required)
// Gating
if ($relay === selfserve_lane_relay::MACHINE) {
// Allowed services are stored as an array of names in lane cache
$allowed = $this->getLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_ALLOWED_SERVICES);
@@ -38,15 +220,10 @@ trait selfserve_lane_relay_controller_t
throw new \Exception("MACHINE relay is not allowed to be enabled at this time");
}
}
// Get the relay ID based on the relay type
$relay_id = match ($relay) {
selfserve_lane_relay::MACHINE => $this->department_lane->relay_machine_id->value(),
default => throw new \Exception("Invalid relay specified, must be MACHINE"),
};
// Make sure relay ID is valid
if (empty($relay_id)) {
throw new \Exception("Invalid relay ID for relay {$relay->name}");
}
$relay_id = $this->getRelayId($relay);
$shelly = new shelly();
$shelly->requireModuleEnabled();
$shelly->requireValidSecretKey();
@@ -77,6 +254,18 @@ trait selfserve_lane_relay_controller_t
* @throws \Exception
*/
public function forceTurnOnMachineRelay(?int $duration = null): bool
{
return $this->forceTurnOnRelay(selfserve_lane_relay::MACHINE, $duration);
}
/**
* Force turn on a specific relay, bypassing allowed services gating.
* @param selfserve_lane_relay $relay
* @param int|null $duration Optional auto-off duration in seconds
* @return bool
* @throws \Exception
*/
public function forceTurnOnRelay(selfserve_lane_relay $relay, ?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}");
@@ -85,10 +274,8 @@ trait selfserve_lane_relay_controller_t
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");
}
$relay_id = $this->getRelayId($relay);
$shelly = new shelly();
$shelly->requireModuleEnabled();
$shelly->requireValidSecretKey();
@@ -118,6 +305,17 @@ trait selfserve_lane_relay_controller_t
* @throws \Exception
*/
public function forceTurnOffMachineRelay(): bool
{
return $this->forceTurnOffRelay(selfserve_lane_relay::MACHINE);
}
/**
* Force turn off a specific relay, bypassing allowed services gating.
* @param selfserve_lane_relay $relay
* @return bool
* @throws \Exception
*/
public function forceTurnOffRelay(selfserve_lane_relay $relay): bool
{
// Require department lane object
if (empty($this->department_lane)) throw new \Exception("Department lane object not found for lane ID {$this->id}");
@@ -126,10 +324,8 @@ trait selfserve_lane_relay_controller_t
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");
}
$relay_id = $this->getRelayId($relay);
$shelly = new shelly();
$shelly->requireModuleEnabled();
$shelly->requireValidSecretKey();
@@ -152,7 +348,7 @@ trait selfserve_lane_relay_controller_t
/**
* Turn off the lane relay
* @param selfserve_lane_relay $relay The relay to turn off (MACHINE)
* @param selfserve_lane_relay $relay The relay to turn off (MACHINE or MACHINE_PROGRAM_PICKER)
* @return bool True if the relay was successfully turned off
* @throws \Exception If an invalid relay is specified or if the lane is not in a state to turn off the relay
*/
@@ -164,15 +360,10 @@ trait selfserve_lane_relay_controller_t
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");
// Get the relay ID based on the relay type
$relay_id = match ($relay) {
selfserve_lane_relay::MACHINE => $this->department_lane->relay_machine_id->value(),
default => throw new \Exception("Invalid relay specified, must be MACHINE"),
};
// Make sure relay ID is valid
if (empty($relay_id)) {
throw new \Exception("Invalid relay ID for relay {$relay->name}");
}
$relay_id = $this->getRelayId($relay);
$shelly = new shelly();
$shelly->requireModuleEnabled();
$shelly->requireValidSecretKey();
@@ -192,4 +383,4 @@ trait selfserve_lane_relay_controller_t
}
return true;
}
}
}