97 lines
2.9 KiB
PHP
97 lines
2.9 KiB
PHP
<?php
|
||
|
||
namespace App\Support\Drivers;
|
||
|
||
use App\Models\Device;
|
||
use App\Models\Entity;
|
||
use App\Support\Mqtt\ShellyTopics;
|
||
use PhpMqtt\Client\Facades\MQTT;
|
||
|
||
/**
|
||
* Controls Shelly Gen2+ devices via JSON-RPC over MQTT on `<prefix>/rpc`
|
||
* (e.g. Switch.Set / Light.Set / Shelly.Reboot). Gen2+ does not accept the simple
|
||
* command topic. The device applies the change and publishes a fresh
|
||
* `<prefix>/status/<component>` which our listener ingests. Vendor specifics stay here (H3).
|
||
*/
|
||
class ShellyMqttDriver implements DeviceDriver
|
||
{
|
||
private const RPC_SRC = 'homeos';
|
||
|
||
public function turnOn(Entity $entity): void
|
||
{
|
||
$this->setComponentOn($entity, true);
|
||
}
|
||
|
||
public function turnOff(Entity $entity): void
|
||
{
|
||
$this->setComponentOn($entity, false);
|
||
}
|
||
|
||
public function setState(Entity $entity, array $state): void
|
||
{
|
||
if (array_key_exists('on', $state)) {
|
||
$this->setComponentOn($entity, (bool) $state['on']);
|
||
}
|
||
}
|
||
|
||
public function reboot(Device $device): void
|
||
{
|
||
$this->rpc($device, 'Shelly.Reboot');
|
||
}
|
||
|
||
/** Light.Set with optional brightness (0–100) and rgb ([r,g,b]) for RGBW lights. */
|
||
public function setLight(Entity $entity, array $params): void
|
||
{
|
||
[, $id] = array_pad(explode(':', $entity->key, 2), 2, '0');
|
||
|
||
$rpc = ['id' => (int) $id];
|
||
if (array_key_exists('on', $params)) {
|
||
$rpc['on'] = (bool) $params['on'];
|
||
}
|
||
if (isset($params['brightness'])) {
|
||
$rpc['brightness'] = max(0, min(100, (int) $params['brightness']));
|
||
}
|
||
if (isset($params['rgb']) && is_array($params['rgb'])) {
|
||
$rpc['rgb'] = array_map(fn ($v) => max(0, min(255, (int) $v)), array_slice($params['rgb'], 0, 3));
|
||
}
|
||
|
||
$this->rpc($entity->device, 'Light.Set', $rpc);
|
||
}
|
||
|
||
public function capabilities(): array
|
||
{
|
||
return ['switch', 'light'];
|
||
}
|
||
|
||
/** Map an entity ("switch:0", "light:0") to the matching Gen2 Set RPC. */
|
||
private function setComponentOn(Entity $entity, bool $on): void
|
||
{
|
||
[$component, $id] = array_pad(explode(':', $entity->key, 2), 2, '0');
|
||
|
||
// Switch.Set / Light.Set — component name capitalised.
|
||
$this->rpc($entity->device, ucfirst($component).'.Set', ['id' => (int) $id, 'on' => $on]);
|
||
}
|
||
|
||
private function rpc(Device $device, string $method, array $params = []): void
|
||
{
|
||
$payload = ['id' => 1, 'src' => self::RPC_SRC, 'method' => $method];
|
||
|
||
if ($params !== []) {
|
||
$payload['params'] = $params;
|
||
}
|
||
|
||
MQTT::connection()->publish(ShellyTopics::rpcTopic($this->prefixOf($device)), json_encode($payload), 0);
|
||
}
|
||
|
||
private function prefixOf(Device $device): string
|
||
{
|
||
$prefix = data_get($device->config, 'mqtt_prefix');
|
||
|
||
if (blank($prefix)) {
|
||
throw new \RuntimeException("Device {$device->id} has no mqtt_prefix configured.");
|
||
}
|
||
|
||
return $prefix;
|
||
}
|
||
}
|