Files
api/services/nginx/app/classes/shelly_relay_inventory.php
T

437 lines
13 KiB
PHP

<?php
namespace classes;
use Exception;
class shelly_relay_inventory
{
private ?shelly $client;
/** @var callable|null */
private $inventory_fetcher = null;
/** @var callable|null */
private $device_list_fetcher = null;
public function __construct(?shelly $client = null)
{
$this->client = $client;
}
public function setInventoryFetcher(callable $fetcher): self
{
$this->inventory_fetcher = $fetcher;
return $this;
}
public function setDeviceListFetcher(callable $fetcher): self
{
$this->device_list_fetcher = $fetcher;
return $this;
}
/**
* @return array<int,array<string,mixed>>
* @throws Exception
*/
public function listRelayOptions(): array
{
$devices_status = $this->fetchOwnedDevicesStatus();
$device_catalog = $this->fetchOwnedDeviceCatalog();
$options_by_id = [];
foreach ($devices_status as $device) {
$normalized_device = $this->normalizeToArray($device);
$device_id = $this->extractFirstString([
$normalized_device['_dev_info']['id'] ?? null,
$normalized_device['id'] ?? null,
]);
$catalog_entry = $device_id !== '' ? ($device_catalog[$device_id] ?? null) : null;
$option = $this->buildRelayOption(
$normalized_device,
is_array($catalog_entry) ? $catalog_entry : null
);
if ($option === null) {
continue;
}
$options_by_id[$option['id']] = $option;
}
$options = array_values($options_by_id);
usort($options, static function (array $left, array $right): int {
$status_compare = self::statusSortWeight((string)($left['status_color'] ?? ''))
<=> self::statusSortWeight((string)($right['status_color'] ?? ''));
if ($status_compare !== 0) {
return $status_compare;
}
$name_compare = strcasecmp((string)($left['name'] ?? ''), (string)($right['name'] ?? ''));
if ($name_compare !== 0) {
return $name_compare;
}
return strcmp((string)($left['id'] ?? ''), (string)($right['id'] ?? ''));
});
return $options;
}
/**
* @return array<string,array<string,mixed>>
*/
private function fetchOwnedDeviceCatalog(): array
{
try {
$payload = is_callable($this->device_list_fetcher)
? ($this->device_list_fetcher)()
: $this->getClient()->sendGetRequest('/interface/device/list', [
'no_shared' => 'true',
]);
} catch (\Throwable) {
return [];
}
$normalized = $this->normalizeToArray($payload);
if (($normalized['isok'] ?? true) === false) {
return [];
}
$devices = $normalized['data']['devices'] ?? null;
if (!is_array($devices)) {
return [];
}
$catalog_by_id = [];
foreach ($devices as $device_key => $device) {
$normalized_device = $this->normalizeToArray($device);
$device_id = $this->extractFirstString([
$normalized_device['id'] ?? null,
is_string($device_key) ? $device_key : null,
]);
if ($device_id === '') {
continue;
}
$catalog_by_id[$device_id] = $normalized_device;
}
return $catalog_by_id;
}
/**
* @return array<string,mixed>
* @throws Exception
*/
private function fetchOwnedDevicesStatus(): array
{
$payload = is_callable($this->inventory_fetcher)
? ($this->inventory_fetcher)()
: $this->getClient()->sendGetRequest('/device/all_status', [
'show_info' => 'true',
'no_shared' => 'true',
]);
$normalized = $this->normalizeToArray($payload);
$is_ok = $normalized['isok'] ?? null;
if ($is_ok === false) {
throw new Exception('Shelly relay inventory request failed');
}
$devices_status = $normalized['data']['devices_status'] ?? null;
if (!is_array($devices_status)) {
throw new Exception('Shelly relay inventory response was missing devices_status');
}
return $devices_status;
}
private function getClient(): shelly
{
if ($this->client instanceof shelly) {
return $this->client;
}
$this->client = new shelly();
return $this->client;
}
/**
* @param array<string,mixed> $device
* @return array<string,mixed>|null
*/
private function buildRelayOption(array $device, ?array $catalog_entry = null): ?array
{
if ($device === [] || !$this->isRelayCapableDevice($device)) {
return null;
}
$device_id = $this->extractFirstString([
$device['_dev_info']['id'] ?? null,
$device['id'] ?? null,
]);
if ($device_id === '') {
return null;
}
$cloud_name = $this->extractFirstString([
$catalog_entry['name'] ?? null,
]);
$local_device_name = $this->extractFirstString([
$device['name'] ?? null,
$device['_dev_info']['name'] ?? null,
$device['settings']['name'] ?? null,
$device['settings']['device']['name'] ?? null,
$device['status']['name'] ?? null,
$device['status']['sys']['device']['name'] ?? null,
]);
$device_name = $cloud_name !== '' ? $cloud_name : $local_device_name;
$device_code = $this->extractFirstString([
$device['_dev_info']['code'] ?? null,
$device['code'] ?? null,
]);
$device_type = $this->extractDeviceType($device, $device_code, $catalog_entry);
$control_type = $this->extractControlType($device);
$control_name = $this->extractControlName($device);
$online = $this->extractOnlineState($device);
if ($online === null) {
$online = $this->normalizeBoolean($catalog_entry['cloud_online'] ?? null);
}
$status_color = $this->extractStatusColor($online);
return [
'id' => $device_id,
'name' => $this->buildRelayLabel(
$device_type,
$cloud_name,
$local_device_name,
$control_name,
$device_id
),
'device_id' => $device_id,
'device_name' => $device_name !== '' ? $device_name : null,
'cloud_name' => $cloud_name !== '' ? $cloud_name : null,
'device_type' => $device_type,
'code' => $device_code !== '' ? $device_code : null,
'control_type' => $control_type,
'control_name' => $control_name !== '' ? $control_name : null,
'status_color' => $status_color,
'online' => $online,
];
}
/**
* @param array<string,mixed> $device
*/
private function isRelayCapableDevice(array $device): bool
{
if ($this->payloadContainsRelayState($device)) {
return true;
}
if ($this->payloadContainsRelayState($device['status'] ?? null)) {
return true;
}
return $this->payloadContainsRelayState($device['settings'] ?? null);
}
private function payloadContainsRelayState(array|object|null $payload): bool
{
$normalized = $this->normalizeToArray($payload);
if ($normalized === []) {
return false;
}
foreach (['switch:0', 'switch_0', 'switch0'] as $switch_key) {
if (array_key_exists($switch_key, $normalized)) {
return true;
}
}
foreach (['relays', 'switches'] as $collection_key) {
if (isset($normalized[$collection_key]) && is_array($normalized[$collection_key]) && $normalized[$collection_key] !== []) {
return true;
}
}
return false;
}
/**
* @param array<string,mixed> $device
*/
private function extractOnlineState(array $device): ?bool
{
$online = $device['_dev_info']['online'] ?? $device['online'] ?? null;
if (is_bool($online)) {
return $online;
}
if (is_numeric($online)) {
return (int)$online === 1;
}
return null;
}
private function extractStatusColor(?bool $online): string
{
if ($online === true) {
return 'Green';
}
if ($online === false) {
return 'Red';
}
return 'Yellow';
}
/**
* @param array<string,mixed> $device
*/
private function extractDeviceType(array $device, string $device_code, ?array $catalog_entry = null): ?string
{
$device_type = $this->extractFirstString([
$device['_dev_info']['model'] ?? null,
$device['_dev_info']['type'] ?? null,
$device['model'] ?? null,
$device['type'] ?? null,
$device['settings']['device']['type'] ?? null,
$catalog_entry['type'] ?? null,
$device_code,
]);
return $device_type !== '' ? $device_type : null;
}
/**
* @param array<string,mixed> $device
*/
private function extractControlType(array $device): string
{
$status = $this->normalizeToArray($device['status'] ?? null);
$settings = $this->normalizeToArray($device['settings'] ?? null);
foreach (['switch:0', 'switch_0', 'switch0'] as $switch_key) {
if (array_key_exists($switch_key, $device) || array_key_exists($switch_key, $status)) {
return 'Switch';
}
}
if ((isset($device['switches']) && is_array($device['switches']) && $device['switches'] !== [])
|| (isset($settings['switches']) && is_array($settings['switches']) && $settings['switches'] !== [])) {
return 'Switch';
}
if ((isset($device['relays']) && is_array($device['relays']) && $device['relays'] !== [])
|| (isset($settings['relays']) && is_array($settings['relays']) && $settings['relays'] !== [])) {
return 'Relay';
}
return 'Device';
}
/**
* @param array<string,mixed> $device
*/
private function extractControlName(array $device): string
{
$status = $this->normalizeToArray($device['status'] ?? null);
$settings = $this->normalizeToArray($device['settings'] ?? null);
return $this->extractFirstString([
$status['switch:0']['name'] ?? null,
$status['switch_0']['name'] ?? null,
$status['switch0']['name'] ?? null,
$device['switches'][0]['name'] ?? null,
$settings['switches'][0]['name'] ?? null,
$device['relays'][0]['name'] ?? null,
$settings['relays'][0]['name'] ?? null,
]);
}
private function buildRelayLabel(
?string $device_type,
string $cloud_name,
string $local_device_name,
string $control_name,
string $device_id
): string {
if ($cloud_name !== '') {
$display_name = $cloud_name;
} elseif ($local_device_name !== '' && $control_name !== '' && strcasecmp($local_device_name, $control_name) !== 0) {
$display_name = $local_device_name . ' / ' . $control_name;
} else {
$display_name = $control_name !== '' ? $control_name : ($local_device_name !== '' ? $local_device_name : $device_id);
}
if ($device_type !== null && $device_type !== '') {
return $display_name . ' (' . $device_type . ')';
}
return $display_name;
}
/**
* @param array<int,mixed> $values
*/
private function extractFirstString(array $values): string
{
foreach ($values as $value) {
$normalized = trim((string)($value ?? ''));
if ($normalized !== '') {
return $normalized;
}
}
return '';
}
private function normalizeBoolean(mixed $value): ?bool
{
if (is_bool($value)) {
return $value;
}
if (is_numeric($value)) {
return (int)$value === 1;
}
return null;
}
private static function statusSortWeight(string $status_color): int
{
return match (strtolower($status_color)) {
'green' => 0,
'yellow' => 1,
'red' => 2,
default => 3,
};
}
/**
* @return array<string,mixed>
*/
private function normalizeToArray(array|object|null $payload): array
{
if (is_array($payload)) {
return $payload;
}
if (is_object($payload)) {
$encoded = json_encode($payload, JSON_UNESCAPED_UNICODE);
if (!is_string($encoded)) {
return [];
}
$decoded = json_decode($encoded, true);
return is_array($decoded) ? $decoded : [];
}
return [];
}
}