Lines
87.15%
156 / 179
Methods
77.27%
34 / 44
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| fromConnectionConfig | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| connect | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| close | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| isConnected | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| onDisconnect | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getHaVersion | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| flushEvents | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getStates | 0.00% | 0 / 6 | 0.00% | 0 / 1 | 20 | ||
| getEntityRegistry | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 4 | ||
| getEntityRegistryEntry | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | ||
| updateEntityRegistryEntry | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 2 | ||
| getAreaRegistry | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getFloorRegistry | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getLabelRegistry | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getDeviceRegistry | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getRegistry | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 1 | ||
| getCurrentUserId | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 3 | ||
| getConfig | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getTimeZone | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| getSiteSettings | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 3 | ||
| getServices | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| callService | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 4 | ||
| fireEvent | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 3 | ||
| fetchHistory | 100.00% | 10 / 10 | 100.00% | 1 / 1 | 6 | ||
| subscribeAllEvents | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 4 | ||
| subscribeTrigger | 100.00% | 9 / 9 | 100.00% | 1 / 1 | 2 | ||
| findComponentVersion | 80.00% | 4 / 5 | 0.00% | 0 / 1 | 4.13 | ||
| subscribeComponentSession | 100.00% | 9 / 9 | 100.00% | 1 / 1 | 2 | ||
| answerComponentCommand | 0.00% | 0 / 6 | 0.00% | 0 / 1 | 12 | ||
| upsertExposedEntity | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| upsertExposedAction | 0.00% | 0 / 3 | 0.00% | 0 / 1 | 6 | ||
| updateExposedEntityState | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| removeExposedEntity | 0.00% | 0 / 2 | 0.00% | 0 / 1 | 6 | ||
| reconcileExposedEntities | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| unsubscribeEvents | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| subscribe | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 3 | ||
| decodeRows | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 4 | ||
| sendUnlessUnknown | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 4 | ||
| toHistoryException | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 6 | ||
| readEntityRegistryEntry | 66.66% | 2 / 3 | 0.00% | 0 / 1 | 3.33 | ||
| toRegistryEditException | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 6 | ||
| toServiceCallException | 100.00% | 12 / 12 | 100.00% | 1 / 1 | 4 | ||
| toEventFireException | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 4 | ||
| 89 | final class HaClient | |
| 90 | { | |
| 91 | private const string UNKNOWN_COMMAND_CODE = 'unknown_command'; | |
| 92 | ||
| 93 | private const string NOT_FOUND_CODE = 'not_found'; | |
| 94 | ||
| 95 | public function __construct( | |
| 96 | private readonly HaConnection $connection, | |
| 97 | private readonly EventDecoder $decoder, | |
| 98 | private readonly EntityStateDecoder $states, | |
| 99 | private readonly RegistryDecoder $registries, | |
| 100 | private readonly ComponentEventDecoder $componentEvents, | |
| 101 | private readonly LoggerInterface $logger = new NullLogger(), | |
| 102 | ) {} | |
| 103 | ||
| 104 | public static function fromConnectionConfig( | |
| 105 | ConnectionConfig $config, | |
| 106 | Deadlines $deadlines, | |
| 107 | LoggerInterface $logger = new NullLogger(), | |
| 108 | ?WebsocketConnector $connector = null, | |
| 109 | ): self { | |
| 110 | $states = new EntityStateDecoder($logger); | |
| 111 | ||
| 112 | return new self(new HaConnection($config, $deadlines, $logger, $connector), new EventDecoder($states), $states, new RegistryDecoder($logger), new ComponentEventDecoder(), $logger); | |
| 113 | } | |
| 114 | ||
| 115 | public function connect(): void | |
| 116 | { | |
| 117 | $this->connection->connect(); | |
| 118 | } | |
| 119 | ||
| 120 | public function close(): void | |
| 121 | { | |
| 122 | $this->connection->close(); | |
| 123 | } | |
| 124 | ||
| 125 | public function isConnected(): bool | |
| 126 | { | |
| 127 | return $this->connection->isConnected(); | |
| 128 | } | |
| 129 | ||
| 130 | /** @param Closure(HaClientException): void $handler */ | |
| 131 | public function onDisconnect(Closure $handler): void | |
| 132 | { | |
| 133 | $this->connection->onDisconnect($handler); | |
| 134 | } | |
| 135 | ||
| 136 | public function getHaVersion(): ?string | |
| 137 | { | |
| 138 | return $this->connection->haVersion; | |
| 139 | } | |
| 140 | ||
| 141 | public function flushEvents(): void | |
| 142 | { | |
| 143 | $this->connection->flushEvents(); | |
| 144 | } | |
| 145 | ||
| 146 | public function getStates(): EntityStateCollection | |
| 147 | { | |
| 148 | $states = []; | |
| 149 | ||
| 150 | foreach ($this->connection->send(new GetStates()) as $entry) { | |
| 151 | $state = \is_array($entry) ? $this->states->decodeEntityStateOrSkip($entry) : null; | |
| 152 | ||
| 153 | if ($state !== null) { | |
| 154 | $states[] = $state; | |
| 155 | } | |
| 156 | } | |
| 157 | ||
| 158 | return EntityStateCollection::keyedByEntityId($states); | |
| 159 | } | |
| 160 | ||
| 161 | public function getEntityRegistry(): EntityRegistryCollection | |
| 162 | { | |
| 163 | $entries = []; | |
| 164 | ||
| 165 | foreach ($this->connection->send(new GetEntityRegistry()) as $entry) { | |
| 166 | if (!\is_array($entry)) { | |
| 167 | continue; | |
| 168 | } | |
| 169 | ||
| 170 | try { | |
| 171 | $entries[] = EntityRegistryEntry::fromArray($entry); | |
| 172 | } catch (IdentifierException $e) { | |
| 173 | $this->logger->debug('Skipped an entity registry entry without a valid entity id', ['exception' => $e]); | |
| 174 | } | |
| 175 | } | |
| 176 | ||
| 177 | return EntityRegistryCollection::keyedByEntityId($entries); | |
| 178 | } | |
| 179 | ||
| 180 | /** @throws RegistryEditException */ | |
| 181 | public function getEntityRegistryEntry(EntityId $entityId): EntityRegistryEntry | |
| 182 | { | |
| 183 | try { | |
| 184 | return $this->readEntityRegistryEntry($this->connection->send(new GetEntityRegistryEntry($entityId)), 'config/entity_registry/get'); | |
| 185 | } catch (HaClientException $e) { | |
| 186 | throw $this->toRegistryEditException($entityId, $e); | |
| 187 | } | |
| 188 | } | |
| 189 | ||
| 190 | /** @throws RegistryEditException */ | |
| 191 | public function updateEntityRegistryEntry(EntityId $entityId, EntityRegistryUpdate $update): EntityRegistryEntry | |
| 192 | { | |
| 193 | try { | |
| 194 | $result = $this->connection->send(new UpdateEntityRegistryEntry($entityId, $update)); | |
| 195 | ||
| 196 | return $this->readEntityRegistryEntry($result['entity_entry'] ?? null, 'config/entity_registry/update'); | |
| 197 | } catch (HaClientException $e) { | |
| 198 | throw $this->toRegistryEditException($entityId, $e); | |
| 199 | } | |
| 200 | } | |
| 201 | ||
| 202 | public function getAreaRegistry(): AreaCollection | |
| 203 | { | |
| 204 | return AreaCollection::keyedByAreaId($this->decodeRows($this->connection->send(new GetAreaRegistry()), $this->registries->decodeAreaOrSkip(...))); | |
| 205 | } | |
| 206 | ||
| 207 | public function getFloorRegistry(): FloorCollection | |
| 208 | { | |
| 209 | return FloorCollection::keyedByFloorId($this->decodeRows($this->sendUnlessUnknown(new GetFloorRegistry()), $this->registries->decodeFloorOrSkip(...))); | |
| 210 | } | |
| 211 | ||
| 212 | public function getLabelRegistry(): LabelCollection | |
| 213 | { | |
| 214 | return LabelCollection::keyedByLabelId($this->decodeRows($this->sendUnlessUnknown(new GetLabelRegistry()), $this->registries->decodeLabelOrSkip(...))); | |
| 215 | } | |
| 216 | ||
| 217 | public function getDeviceRegistry(): DeviceCollection | |
| 218 | { | |
| 219 | return DeviceCollection::keyedByDeviceId($this->decodeRows($this->connection->send(new GetDeviceRegistry()), $this->registries->decodeDeviceOrSkip(...))); | |
| 220 | } | |
| 221 | ||
| 222 | public function getRegistry(): IndexedRegistry | |
| 223 | { | |
| 224 | return IndexedRegistry::fromParts( | |
| 225 | $this->getAreaRegistry(), | |
| 226 | $this->getFloorRegistry(), | |
| 227 | $this->getLabelRegistry(), | |
| 228 | $this->getDeviceRegistry(), | |
| 229 | $this->getEntityRegistry()->toRegisteredEntities(), | |
| 230 | ); | |
| 231 | } | |
| 232 | ||
| 233 | /** @throws HaClientException */ | |
| 234 | public function getCurrentUserId(): string | |
| 235 | { | |
| 236 | $id = $this->connection->send(new GetCurrentUser())['id'] ?? null; | |
| 237 | ||
| 238 | return \is_string($id) && $id !== '' ? $id : throw HaClientException::protocolViolation('a current user without an id'); | |
| 239 | } | |
| 240 | ||
| 241 | public function getConfig(): HaConfig | |
| 242 | { | |
| 243 | return HaConfig::fromGetConfigResult($this->connection->send(new GetConfig())); | |
| 244 | } | |
| 245 | ||
| 246 | public function getTimeZone(): DateTimeZone | |
| 247 | { | |
| 248 | return $this->getSiteSettings()->timeZone; | |
| 249 | } | |
| 250 | ||
| 251 | public function getSiteSettings(): HaSiteSettings | |
| 252 | { | |
| 253 | $config = $this->getConfig(); | |
| 254 | ||
| 255 | if ($config->timeZone === null && $config->timeZoneName !== null) { | |
| 256 | $this->logger->warning('Home Assistant reported an unusable timezone; falling back to UTC.', [ | |
| 257 | 'time_zone' => $config->timeZoneName, | |
| 258 | ]); | |
| 259 | } | |
| 260 | ||
| 261 | return new HaSiteSettings($config->timeZone ?? new DateTimeZone('UTC'), $config->location); | |
| 262 | } | |
| 263 | ||
| 264 | /** @return array<string, mixed> */ | |
| 265 | public function getServices(): array | |
| 266 | { | |
| 267 | return JsonShape::treatKeysAsStrings($this->connection->send(new GetServices())); | |
| 268 | } | |
| 269 | ||
| 270 | /** | |
| 271 | * @param array<string, mixed> $data | |
| 272 | * @throws ServiceCallException | |
| 273 | */ | |
| 274 | public function callService( | |
| 275 | string $domain, | |
| 276 | string $service, | |
| 277 | array $data = [], | |
| 278 | ?ServiceTarget $target = null, | |
| 279 | bool $returnResponse = false, | |
| 280 | ): ServiceResponse { | |
| 281 | try { | |
| 282 | $result = $this->connection->send(new CallService($domain, $service, $data, $target, $returnResponse)); | |
| 283 | } catch (HaClientException $e) { | |
| 284 | throw $this->toServiceCallException($domain, $service, $e); | |
| 285 | } | |
| 286 | ||
| 287 | $response = $result['response'] ?? null; | |
| 288 | $context = $result['context'] ?? null; | |
| 289 | ||
| 290 | return new ServiceResponse( | |
| 291 | $domain, | |
| 292 | $service, | |
| 293 | \is_array($response) ? JsonShape::treatKeysAsStrings($response) : [], | |
| 294 | \is_array($context) ? EventContext::fromArray($context) : null, | |
| 295 | ); | |
| 296 | } | |
| 297 | ||
| 298 | /** @throws EventFireException */ | |
| 299 | public function fireEvent(EventPayload $payload): EventContext | |
| 300 | { | |
| 301 | try { | |
| 302 | $result = $this->connection->send(new FireEvent($payload)); | |
| 303 | } catch (HaClientException $e) { | |
| 304 | throw $this->toEventFireException($payload->eventType, $e); | |
| 305 | } | |
| 306 | ||
| 307 | $context = $result['context'] ?? null; | |
| 308 | ||
| 309 | return \is_array($context) ? EventContext::fromArray($context) : EventContext::unknown(); | |
| 310 | } | |
| 311 | ||
| 312 | /** @throws HistoryException */ | |
| 313 | public function fetchHistory(EntityId $entityId, HistoryWindow $window, HistoryDetail $detail): EntityStateHistory | |
| 314 | { | |
| 315 | try { | |
| 316 | $result = $this->connection->send(new GetHistoryDuringPeriod($entityId, $window, $detail)); | |
| 317 | } catch (HaClientException $e) { | |
| 318 | throw $this->toHistoryException($entityId, $e); | |
| 319 | } | |
| 320 | ||
| 321 | $rows = $result[$entityId->value] ?? []; | |
| 322 | $states = []; | |
| 323 | ||
| 324 | foreach (\is_array($rows) ? $rows : [] as $row) { | |
| 325 | $state = \is_array($row) ? $this->states->decodeCompressedHistoryRowOrSkip($entityId, $row) : null; | |
| 326 | ||
| 327 | if ($state !== null) { | |
| 328 | $states[] = $state; | |
| 329 | } | |
| 330 | } | |
| 331 | ||
| 332 | return new EntityStateHistory($entityId, $window, HistoricalStateCollection::fromList($states)); | |
| 333 | } | |
| 334 | ||
| 335 | /** | |
| 336 | * @param Closure(StateChange): void $onStateChange | |
| 337 | * @param Closure(HaEvent): void $onEvent | |
| 338 | * @throws HaClientException | |
| 339 | */ | |
| 340 | public function subscribeAllEvents(Closure $onStateChange, Closure $onEvent): int | |
| 341 | { | |
| 342 | $decoder = $this->decoder; | |
| 343 | ||
| 344 | return $this->subscribe(new SubscribeEvents(), static function (array $event) use ($decoder, $onStateChange, $onEvent): void { | |
| 345 | if (($event['event_type'] ?? null) === HaEvent::STATE_CHANGED) { | |
| 346 | $change = $decoder->decodeStateChange($event); | |
| 347 | ||
| 348 | if ($change !== null) { | |
| 349 | $onStateChange($change); | |
| 350 | } | |
| 351 | ||
| 352 | return; | |
| 353 | } | |
| 354 | ||
| 355 | $decoded = $decoder->decodeEvent($event); | |
| 356 | ||
| 357 | if ($decoded !== null) { | |
| 358 | $onEvent($decoded); | |
| 359 | } | |
| 360 | }); | |
| 361 | } | |
| 362 | ||
| 363 | /** | |
| 364 | * @param array<string, mixed> $variables | |
| 365 | * @param Closure(TriggerEvent): void $onTrigger | |
| 366 | * @throws HaClientException | |
| 367 | */ | |
| 368 | public function subscribeTrigger(HaTriggerCollection $triggers, array $variables, Closure $onTrigger): int | |
| 369 | { | |
| 370 | $decoder = $this->decoder; | |
| 371 | $logger = $this->logger; | |
| 372 | ||
| 373 | return $this->subscribe(new SubscribeTrigger($triggers, $variables), static function (array $event) use ($decoder, $logger, $onTrigger): void { | |
| 374 | $decoded = $decoder->decodeTriggerEvent($event); | |
| 375 | ||
| 376 | if ($decoded === null) { | |
| 377 | $logger->warning('Skipped a trigger event without variables.trigger'); | |
| 378 | ||
| 379 | return; | |
| 380 | } | |
| 381 | ||
| 382 | $onTrigger($decoded); | |
| 383 | }); | |
| 384 | } | |
| 385 | ||
| 386 | /** @throws HaClientException */ | |
| 387 | public function findComponentVersion(): ?ComponentVersion | |
| 388 | { | |
| 389 | try { | |
| 390 | return ComponentVersion::fromVersionResult($this->connection->send(new GetComponentVersion())); | |
| 391 | } catch (HaClientException $e) { | |
| 392 | if ($e->reason === HaClientError::CommandRejected && $e->findErrorCode() === self::UNKNOWN_COMMAND_CODE) { | |
| 393 | return null; | |
| 394 | } | |
| 395 | ||
| 396 | throw $e; | |
| 397 | } | |
| 398 | } | |
| 399 | ||
| 400 | /** | |
| 401 | * @param Closure(ComponentSessionEvent): void $onEvent | |
| 402 | * @throws HaClientException | |
| 403 | */ | |
| 404 | public function subscribeComponentSession(ComponentSessionRequest $request, Closure $onEvent): int | |
| 405 | { | |
| 406 | $decoder = $this->componentEvents; | |
| 407 | $logger = $this->logger; | |
| 408 | ||
| 409 | return $this->subscribe(new SubscribeComponentSession($request), static function (array $event) use ($decoder, $logger, $onEvent): void { | |
| 410 | $decoded = $decoder->decodeSessionEvent($event); | |
| 411 | ||
| 412 | if ($decoded === null) { | |
| 413 | $logger->debug('Skipped a Stewart component event of an unknown type', ['type' => $event['type'] ?? null]); | |
| 414 | ||
| 415 | return; | |
| 416 | } | |
| 417 | ||
| 418 | $onEvent($decoded); | |
| 419 | }); | |
| 420 | } | |
| 421 | ||
| 422 | /** @throws HaClientException */ | |
| 423 | public function answerComponentCommand(ComponentCommandAnswer $answer): bool | |
| 424 | { | |
| 425 | try { | |
| 426 | $this->connection->sendIgnoringResult(new AnswerComponentCommand($answer)); | |
| 427 | } catch (HaClientException $e) { | |
| 428 | if (ComponentErrorCode::tryFromException($e) === ComponentErrorCode::NotFound) { | |
| 429 | return false; | |
| 430 | } | |
| 431 | ||
| 432 | throw $e; | |
| 433 | } | |
| 434 | ||
| 435 | return true; | |
| 436 | } | |
| 437 | ||
| 438 | /** @throws HaClientException */ | |
| 439 | public function upsertExposedEntity(ExposedEntityAddress $address, ExposedEntityDefinition $definition, ExposedStateChange $change): ExposedEntitySnapshot | |
| 440 | { | |
| 441 | return ExposedEntitySnapshotReader::readUpsertResult($this->connection->send(new UpsertExposedEntity($address, $definition, $change))); | |
| 442 | } | |
| 443 | ||
| 444 | // An action registers a service, not an entity, so it has no snapshot to read back. | |
| 445 | /** @throws HaClientException */ | |
| 446 | public function upsertExposedAction(ExposedEntityAddress $address, ExposedEntityDefinition $definition): void | |
| 447 | { | |
| 448 | $result = $this->connection->send(new UpsertExposedEntity($address, $definition, new ExposedStateChange())); | |
| 449 | ||
| 450 | if (!\is_string($result['service'] ?? null)) { | |
| 451 | throw HaClientException::protocolViolation('a stewart/entity/upsert result for an action without service'); | |
| 452 | } | |
| 453 | } | |
| 454 | ||
| 455 | /** @throws HaClientException */ | |
| 456 | public function updateExposedEntityState(ExposedEntityAddress $address, ExposedStateChange $change): void | |
| 457 | { | |
| 458 | $this->connection->sendIgnoringResult(new UpdateExposedEntityState($address, $change)); | |
| 459 | } | |
| 460 | ||
| 461 | /** @throws HaClientException */ | |
| 462 | public function removeExposedEntity(ExposedEntityAddress $address): bool | |
| 463 | { | |
| 464 | $removed = $this->connection->send(new RemoveExposedEntity($address))['removed'] ?? null; | |
| 465 | ||
| 466 | return \is_bool($removed) ? $removed : throw HaClientException::protocolViolation('a stewart/entity/remove result without removed'); | |
| 467 | } | |
| 468 | ||
| 469 | /** @throws HaClientException */ | |
| 470 | public function reconcileExposedEntities(ExposedEntityReconcile $reconcile): EntityIdCollection | |
| 471 | { | |
| 472 | return ReconcileResultReader::readRemovedEntityIds($this->connection->send(new ReconcileExposedEntities($reconcile))); | |
| 473 | } | |
| 474 | ||
| 475 | /** @throws HaClientException */ | |
| 476 | public function unsubscribeEvents(int $subscriptionId): void | |
| 477 | { | |
| 478 | $this->connection->unsubscribeEvents($subscriptionId); | |
| 479 | } | |
| 480 | ||
| 481 | /** | |
| 482 | * @param Closure(array<string, mixed>): void $handler | |
| 483 | * @throws HaClientException | |
| 484 | */ | |
| 485 | private function subscribe(SubscriptionCommand $command, Closure $handler): int | |
| 486 | { | |
| 487 | try { | |
| 488 | return $this->connection->subscribeEvents($command, $handler); | |
| 489 | } catch (HaClientException $e) { | |
| 490 | throw $e->reason === HaClientError::CommandUnauthorized | |
| 491 | ? HaClientException::createForReason(HaClientError::AdministratorRequired, $e) | |
| 492 | : $e; | |
| 493 | } | |
| 494 | } | |
| 495 | ||
| 496 | /** | |
| 497 | * @template T of object | |
| 498 | * @param array<array-key, mixed> $rows | |
| 499 | * @param Closure(array<array-key, mixed>): ?T $decodeOrSkip | |
| 500 | * @return list<T> | |
| 501 | */ | |
| 502 | private function decodeRows(array $rows, Closure $decodeOrSkip): array | |
| 503 | { | |
| 504 | $decoded = []; | |
| 505 | ||
| 506 | foreach ($rows as $row) { | |
| 507 | $entry = \is_array($row) ? $decodeOrSkip($row) : null; | |
| 508 | ||
| 509 | if ($entry !== null) { | |
| 510 | $decoded[] = $entry; | |
| 511 | } | |
| 512 | } | |
| 513 | ||
| 514 | return $decoded; | |
| 515 | } | |
| 516 | ||
| 517 | /** @return array<array-key, mixed> */ | |
| 518 | private function sendUnlessUnknown(HaCommand $command): array | |
| 519 | { | |
| 520 | // Floors and labels arrived in Home Assistant 2024.4. | |
| 521 | try { | |
| 522 | return $this->connection->send($command); | |
| 523 | } catch (HaClientException $e) { | |
| 524 | if ($e->reason === HaClientError::CommandRejected && $e->findErrorCode() === self::UNKNOWN_COMMAND_CODE) { | |
| 525 | return []; | |
| 526 | } | |
| 527 | ||
| 528 | throw $e; | |
| 529 | } | |
| 530 | } | |
| 531 | ||
| 532 | private function toHistoryException(EntityId $entityId, HaClientException $e): HistoryException | |
| 533 | { | |
| 534 | if ($e->reason === HaClientError::CommandRejected && $e->findErrorCode() === self::UNKNOWN_COMMAND_CODE) { | |
| 535 | return HistoryException::createForReason(HistoryError::RecorderUnavailable, $e, entityId: $entityId->value); | |
| 536 | } | |
| 537 | ||
| 538 | return match ($e->reason) { | |
| 539 | HaClientError::CommandRejected, | |
| 540 | HaClientError::CommandUnauthorized, | |
| 541 | HaClientError::CommandUnencodable => HistoryException::rejected($entityId, $e->findDetail() ?? $e->getMessage(), $e->findErrorCode(), $e), | |
| 542 | HaClientError::CommandTimedOut => HistoryException::timedOut($entityId, $e->getMessage(), $e), | |
| 543 | default => HistoryException::unreachable($entityId, $e->getMessage(), $e), | |
| 544 | }; | |
| 545 | } | |
| 546 | ||
| 547 | /** @throws HaClientException */ | |
| 548 | private function readEntityRegistryEntry(mixed $row, string $command): EntityRegistryEntry | |
| 549 | { | |
| 550 | try { | |
| 551 | return \is_array($row) ? EntityRegistryEntry::fromArray($row) : throw HaClientException::protocolViolation(\sprintf('a %s result without an entity entry', $command)); | |
| 552 | } catch (IdentifierException $e) { | |
| 553 | throw HaClientException::protocolViolation(\sprintf('a %s result without a valid entity id', $command), $e); | |
| 554 | } | |
| 555 | } | |
| 556 | ||
| 557 | private function toRegistryEditException(EntityId $entityId, HaClientException $e): RegistryEditException | |
| 558 | { | |
| 559 | if ($e->reason === HaClientError::CommandRejected && $e->findErrorCode() === self::NOT_FOUND_CODE) { | |
| 560 | return RegistryEditException::notFound($entityId); | |
| 561 | } | |
| 562 | ||
| 563 | return match ($e->reason) { | |
| 564 | HaClientError::CommandRejected, | |
| 565 | HaClientError::CommandUnauthorized, | |
| 566 | HaClientError::CommandUnencodable => RegistryEditException::rejected($entityId, $e->findDetail() ?? $e->getMessage(), $e->findErrorCode(), $e), | |
| 567 | HaClientError::CommandTimedOut => RegistryEditException::timedOut($entityId, $e->getMessage(), $e), | |
| 568 | default => RegistryEditException::unreachable($entityId, $e->getMessage(), $e), | |
| 569 | }; | |
| 570 | } | |
| 571 | ||
| 572 | private function toServiceCallException(string $domain, string $service, HaClientException $e): ServiceCallException | |
| 573 | { | |
| 574 | return match ($e->reason) { | |
| 575 | HaClientError::CommandRejected, | |
| 576 | HaClientError::CommandUnauthorized, | |
| 577 | HaClientError::CommandUnencodable => ServiceCallException::rejected( | |
| 578 | $domain, | |
| 579 | $service, | |
| 580 | $e->findDetail() ?? $e->getMessage(), | |
| 581 | $e->findErrorCode(), | |
| 582 | $e, | |
| 583 | ), | |
| 584 | HaClientError::CommandTimedOut => ServiceCallException::timedOut($domain, $service, $e->getMessage(), $e), | |
| 585 | default => ServiceCallException::unreachable($domain, $service, $e->getMessage(), $e), | |
| 586 | }; | |
| 587 | } | |
| 588 | ||
| 589 | private function toEventFireException(string $eventType, HaClientException $e): EventFireException | |
| 590 | { | |
| 591 | return match ($e->reason) { | |
| 592 | HaClientError::CommandRejected, | |
| 593 | HaClientError::CommandUnauthorized, | |
| 594 | HaClientError::CommandUnencodable => EventFireException::rejected($eventType, $e->findDetail() ?? $e->getMessage(), $e->findErrorCode(), $e), | |
| 595 | HaClientError::CommandTimedOut => EventFireException::timedOut($eventType, $e->getMessage(), $e), | |
| 596 | default => EventFireException::unreachable($eventType, $e->getMessage(), $e), | |
| 597 | }; | |
| 598 | } | |
| 599 | } |