diff --git a/app/Support/Drivers/ShellyMqttDriver.php b/app/Support/Drivers/ShellyMqttDriver.php index 27d4cff..bf0bab2 100644 --- a/app/Support/Drivers/ShellyMqttDriver.php +++ b/app/Support/Drivers/ShellyMqttDriver.php @@ -8,37 +8,35 @@ use App\Support\Mqtt\ShellyTopics; use PhpMqtt\Client\Facades\MQTT; /** - * Controls Shelly Gen2+ devices over MQTT via the simple command topic - * `/command/` = on|off|toggle. Vendor specifics stay here (H3). + * Controls Shelly Gen2+ devices via JSON-RPC over MQTT on `/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 + * `/status/` 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->publishCommand($entity, 'on'); + $this->setComponentOn($entity, true); } public function turnOff(Entity $entity): void { - $this->publishCommand($entity, 'off'); + $this->setComponentOn($entity, false); } public function setState(Entity $entity, array $state): void { if (array_key_exists('on', $state)) { - $state['on'] ? $this->turnOn($entity) : $this->turnOff($entity); + $this->setComponentOn($entity, (bool) $state['on']); } } public function reboot(Device $device): void { - $prefix = $this->prefixOf($device); - - MQTT::connection()->publish( - ShellyTopics::rpcTopic($prefix), - json_encode(['id' => 1, 'src' => 'homeos', 'method' => 'Shelly.Reboot']), - 0, - ); + $this->rpc($device, 'Shelly.Reboot'); } public function capabilities(): array @@ -46,13 +44,24 @@ class ShellyMqttDriver implements DeviceDriver return ['switch', 'light']; } - private function publishCommand(Entity $entity, string $value): void + /** Map an entity ("switch:0", "light:0") to the matching Gen2 Set RPC. */ + private function setComponentOn(Entity $entity, bool $on): void { - MQTT::connection()->publish( - ShellyTopics::commandTopic($this->prefixOf($entity->device), $entity->key), - $value, - 0, - ); + [$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 diff --git a/app/Support/Mqtt/ShellyTopics.php b/app/Support/Mqtt/ShellyTopics.php index d150fa2..f2113a3 100644 --- a/app/Support/Mqtt/ShellyTopics.php +++ b/app/Support/Mqtt/ShellyTopics.php @@ -29,13 +29,7 @@ class ShellyTopics return [$m['prefix'], $m['component']]; } - /** Simple command topic for a component, e.g. "/command/switch:0" => "on|off|toggle". */ - public static function commandTopic(string $prefix, string $component): string - { - return "{$prefix}/command/{$component}"; - } - - /** JSON-RPC topic for device-level methods (e.g. Shelly.Reboot). */ + /** JSON-RPC topic for control (Switch.Set, Light.Set, Shelly.Reboot, …). */ public static function rpcTopic(string $prefix): string { return "{$prefix}/rpc";