Improve error handling, availability logic and state restore

- Add TransferResult enum (SUCCESS, SENT, SERVER_ERROR, NETWORK_ERROR)
  to distinguish network errors from server errors
- Only mark entities as unavailable on NETWORK_ERROR (addon unreachable)
- HTTP 401, 405, 500 no longer trigger unavailable state, logged instead
- Add RestoreEntity to cover, switch and light to restore last known
  state after HA restart
This commit is contained in:
Daniel Moindrot
2026-06-12 08:09:24 +02:00
committed by GitHub
parent 6949219c2b
commit 9047b75ce6
4 changed files with 83 additions and 73 deletions
+34 -14
View File
@@ -13,12 +13,22 @@ RTYPE_LABELS = {
4096: "AirSend Button",
4097: "AirSend Switch",
4098: "AirSend Cover",
4099: "AirSend Slider",
4100: "AirSend Tilt",
4101: "AirSend Light",
4099: "AirSend Cover (position)",
4100: "AirSend Light",
}
from enum import Enum
class TransferResult(Enum):
"""Result of an async_transfer call."""
SUCCESS = "success" # Command sent and confirmed
SENT = "sent" # Command sent, no confirmation (wait=True + 500)
SERVER_ERROR = "server_error" # Server responded with error (command may have been sent)
NETWORK_ERROR = "network_error" # Addon unreachable (timeout, connection refused)
class Device:
"""Representation of a Device."""
@@ -91,7 +101,7 @@ class Device:
if self._channel:
result = str(self._channel['id'])
if result:
uniquefield = ['source', 'mac', 'seed', 'token']
uniquefield = ['source', 'mac', 'seed']
for field in uniquefield:
if field in self._channel:
result += "_"
@@ -146,7 +156,7 @@ class Device:
@property
def is_light(self) -> bool:
return self._rtype == 4101
return self._rtype == 4100
@property
def refresh_value(self) -> int:
@@ -182,7 +192,7 @@ class Device:
_LOGGER.debug("Bind error '%s': %s", self._name, err)
return False
async def async_transfer(self, note, entity_id=None) -> bool:
async def async_transfer(self, note, entity_id=None) -> "TransferResult":
"""Send a command (async)."""
status_code = 404
ret = False
@@ -236,6 +246,7 @@ class Device:
status_code = response.status
except aiohttp.ClientError as err:
_LOGGER.debug("Transfer local error '%s': %s", self._name, err)
return TransferResult.NETWORK_ERROR
# Fallback to cloud API if local failed
if status_code != 200 and self._apikey:
@@ -269,13 +280,22 @@ class Device:
status_code = response.status
ret = True
except aiohttp.ClientError as err:
_LOGGER.debug("Transfer cloud error '%s': %s", self._name, err)
_LOGGER.warning("Transfer cloud error '%s': %s — both local and cloud failed", self._name, err)
return TransferResult.NETWORK_ERROR
if status_code == 200:
return ret
# 500 with wait=True means RF confirmation not received but command was sent
if status_code == 500 and self._wait:
_LOGGER.warning("Transfer '%s' : no RF confirmation (500), command may have been sent", self.name)
return True
_LOGGER.error("Transfer error '%s' : '%s'", self.name, status_code)
return False
return TransferResult.SUCCESS
if status_code == 401:
_LOGGER.error("Transfer '%s' : invalid locator (401) — check spurl in airsend.yaml", self.name)
return TransferResult.SERVER_ERROR
if status_code == 405:
_LOGGER.error("Transfer '%s' : invalid input (405) — check channel configuration", self.name)
return TransferResult.SERVER_ERROR
if status_code == 500:
if self._wait:
_LOGGER.warning("Transfer '%s' : no RF confirmation (500), command may have been sent", self.name)
return TransferResult.SENT
_LOGGER.warning("Transfer '%s' : server error (500), command may have been sent", self.name)
return TransferResult.SERVER_ERROR
_LOGGER.warning("Transfer '%s' : unexpected status %s", self.name, status_code)
return TransferResult.SERVER_ERROR