1
0
mirror of https://github.com/natekspencer/hacs-oasis_mini.git synced 2025-12-06 18:44:14 -05:00

Adjust exceptions

This commit is contained in:
Nathan Spencer
2025-11-24 04:06:42 +00:00
parent 1cc3585653
commit e4f6cd2803
8 changed files with 106 additions and 109 deletions

View File

@@ -46,9 +46,9 @@ def setup_platform_from_coordinator(
) -> None: ) -> None:
""" """
Populate entities for devices managed by the coordinator and add entities for any devices discovered later. Populate entities for devices managed by the coordinator and add entities for any devices discovered later.
This registers a listener on the coordinator to detect newly discovered devices by serial number and calls `make_entities` to construct entity objects for those devices, passing them to `async_add_entities`. The initial device set is processed immediately; subsequent discoveries are handled via the coordinator listener. This registers a listener on the coordinator to detect newly discovered devices by serial number and calls `make_entities` to construct entity objects for those devices, passing them to `async_add_entities`. The initial device set is processed immediately; subsequent discoveries are handled via the coordinator listener.
Parameters: Parameters:
entry: Config entry containing the coordinator in its `runtime_data`. entry: Config entry containing the coordinator in its `runtime_data`.
async_add_entities: Home Assistant callback to add entities to the platform. async_add_entities: Home Assistant callback to add entities to the platform.
@@ -63,7 +63,7 @@ def setup_platform_from_coordinator(
def _check_devices() -> None: def _check_devices() -> None:
""" """
Detect newly discovered Oasis devices from the coordinator and register their entities. Detect newly discovered Oasis devices from the coordinator and register their entities.
Scans the coordinator's current device list for devices with a serial number that has not Scans the coordinator's current device list for devices with a serial number that has not
been seen before. For any newly discovered devices, creates entity instances via been seen before. For any newly discovered devices, creates entity instances via
make_entities and adds them to Home Assistant using async_add_entities with the make_entities and adds them to Home Assistant using async_add_entities with the
@@ -95,7 +95,7 @@ def setup_platform_from_coordinator(
async def async_setup_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry) -> bool:
""" """
Initialize Oasis cloud and MQTT integration for a config entry, create and refresh the device coordinator, register update listeners for discovered devices, forward platform setup, and update the entry's metadata as needed. Initialize Oasis cloud and MQTT integration for a config entry, create and refresh the device coordinator, register update listeners for discovered devices, forward platform setup, and update the entry's metadata as needed.
Returns: Returns:
True if the config entry was set up successfully. True if the config entry was set up successfully.
""" """
@@ -110,10 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry)
coordinator = OasisDeviceCoordinator(hass, cloud_client, mqtt_client) coordinator = OasisDeviceCoordinator(hass, cloud_client, mqtt_client)
try: await coordinator.async_config_entry_first_refresh()
await coordinator.async_config_entry_first_refresh()
except Exception as ex:
_LOGGER.exception(ex)
if entry.unique_id != (user_id := str(user["id"])): if entry.unique_id != (user_id := str(user["id"])):
hass.config_entries.async_update_entry(entry, unique_id=user_id) hass.config_entries.async_update_entry(entry, unique_id=user_id)
@@ -126,13 +123,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry)
def _on_oasis_update() -> None: def _on_oasis_update() -> None:
""" """
Update the coordinator's last-updated timestamp and notify its listeners. Update the coordinator's last-updated timestamp and notify its listeners.
Sets the coordinator's last_updated to the current time and triggers its update listeners so dependent entities and tasks refresh. Sets the coordinator's last_updated to the current time and triggers its update listeners so dependent entities and tasks refresh.
""" """
coordinator.last_updated = dt_util.now() coordinator.last_updated = dt_util.now()
coordinator.async_update_listeners() coordinator.async_update_listeners()
for device in coordinator.data: for device in coordinator.data or []:
device.add_update_listener(_on_oasis_update) device.add_update_listener(_on_oasis_update)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -145,9 +142,9 @@ async def async_unload_entry(
) -> bool: ) -> bool:
""" """
Cleanly unload an Oasis device config entry. Cleanly unload an Oasis device config entry.
Closes the MQTT and cloud clients stored on the entry and unloads all supported platforms. Closes the MQTT and cloud clients stored on the entry and unloads all supported platforms.
Returns: Returns:
`True` if all platforms were unloaded successfully, `False` otherwise. `True` if all platforms were unloaded successfully, `False` otherwise.
""" """
@@ -165,29 +162,29 @@ async def async_remove_entry(
) -> None: ) -> None:
""" """
Perform logout and cleanup for the cloud client associated with the config entry. Perform logout and cleanup for the cloud client associated with the config entry.
Attempts to call the cloud client's logout method and logs any exception encountered, then ensures the client is closed. Attempts to call the cloud client's logout method and logs any exception encountered, then ensures the client is closed.
""" """
cloud_client = create_client(hass, entry.data) cloud_client = create_client(hass, entry.data)
try: try:
await cloud_client.async_logout() await cloud_client.async_logout()
except Exception as ex: except Exception:
_LOGGER.exception(ex) _LOGGER.exception("Error attempting to logout from the cloud")
await cloud_client.async_close() await cloud_client.async_close()
async def async_migrate_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry): async def async_migrate_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry):
""" """
Migrate an Oasis config entry to the current schema (minor version 3). Migrate an Oasis config entry to the current schema (minor version 3).
Performs in-place migrations for older entries: Performs in-place migrations for older entries:
- Renames select entity unique IDs ending with `-playlist` to `-queue`. - Renames select entity unique IDs ending with `-playlist` to `-queue`.
- When migrating to the auth-required schema, moves relevant options into entry data and clears options. - When migrating to the auth-required schema, moves relevant options into entry data and clears options.
- Updates the config entry's data, options, minor_version, title (from CONF_EMAIL or "Oasis Control"), unique_id, and version. - Updates the config entry's data, options, minor_version, title (from CONF_EMAIL or "Oasis Control"), unique_id, and version.
Parameters: Parameters:
entry: The config entry to migrate. entry: The config entry to migrate.
Returns: Returns:
`True` if migration succeeded, `False` if migration could not be performed (e.g., entry.version is greater than supported). `True` if migration succeeded, `False` if migration could not be performed (e.g., entry.version is greater than supported).
""" """
@@ -211,10 +208,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry
) -> dict[str, Any] | None: ) -> dict[str, Any] | None:
""" """
Update a registry entry's unique_id suffix from "-playlist" to "-queue" when applicable. Update a registry entry's unique_id suffix from "-playlist" to "-queue" when applicable.
Parameters: Parameters:
entity_entry (er.RegistryEntry): Registry entry to inspect. entity_entry (er.RegistryEntry): Registry entry to inspect.
Returns: Returns:
dict[str, Any] | None: A mapping {"new_unique_id": <new id>} if the entry is in the "select" domain and its unique_id ends with "-playlist"; otherwise `None`. dict[str, Any] | None: A mapping {"new_unique_id": <new id>} if the entry is in the "select" domain and its unique_id ends with "-playlist"; otherwise `None`.
""" """
@@ -256,11 +253,11 @@ async def async_remove_config_entry_device(
) -> bool: ) -> bool:
""" """
Determine whether the config entry is no longer associated with the given device. Determine whether the config entry is no longer associated with the given device.
Parameters: Parameters:
config_entry (OasisDeviceConfigEntry): The config entry whose runtime data contains device serial numbers. config_entry (OasisDeviceConfigEntry): The config entry whose runtime data contains device serial numbers.
device_entry (DeviceEntry): The device registry entry to check for matching identifiers. device_entry (DeviceEntry): The device registry entry to check for matching identifiers.
Returns: Returns:
bool: `true` if none of the device's identifiers match serial numbers present in the config entry's runtime data, `false` otherwise. bool: `true` if none of the device's identifiers match serial numbers present in the config entry's runtime data, `false` otherwise.
""" """
@@ -269,4 +266,4 @@ async def async_remove_config_entry_device(
identifier identifier
for identifier in device_entry.identifiers for identifier in device_entry.identifiers
if identifier[0] == DOMAIN and identifier[1] in current_serials if identifier[0] == DOMAIN and identifier[1] in current_serials
) )

View File

@@ -35,10 +35,10 @@ class OasisDeviceConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult: ) -> ConfigFlowResult:
""" """
Begin the reauthentication flow for an existing config entry. Begin the reauthentication flow for an existing config entry.
Parameters: Parameters:
entry_data (Mapping[str, Any]): Data from the existing config entry that triggered the reauthentication flow. entry_data (Mapping[str, Any]): Data from the existing config entry that triggered the reauthentication flow.
Returns: Returns:
ConfigFlowResult: Result that presents the reauthentication confirmation dialog to the user. ConfigFlowResult: Result that presents the reauthentication confirmation dialog to the user.
""" """
@@ -49,9 +49,9 @@ class OasisDeviceConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult: ) -> ConfigFlowResult:
""" """
Present a reauthentication confirmation form to the user. Present a reauthentication confirmation form to the user.
If `user_input` is provided it will be used as the form values; otherwise the existing entry's data are used as suggested values. If `user_input` is provided it will be used as the form values; otherwise the existing entry's data are used as suggested values.
Returns: Returns:
ConfigFlowResult: Result of the config flow step that renders the reauthentication form or advances the flow. ConfigFlowResult: Result of the config flow step that renders the reauthentication form or advances the flow.
""" """
@@ -68,10 +68,10 @@ class OasisDeviceConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult: ) -> ConfigFlowResult:
""" """
Handle the initial user configuration step for the Oasis integration. Handle the initial user configuration step for the Oasis integration.
Parameters: Parameters:
user_input (dict[str, Any] | None): Optional prefilled values (e.g., `email`, `password`) submitted by the user. user_input (dict[str, Any] | None): Optional prefilled values (e.g., `email`, `password`) submitted by the user.
Returns: Returns:
ConfigFlowResult: Result of the "user" step — a form prompting for credentials, an abort, or a created/updated config entry. ConfigFlowResult: Result of the "user" step — a form prompting for credentials, an abort, or a created/updated config entry.
""" """
@@ -100,15 +100,15 @@ class OasisDeviceConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult: ) -> ConfigFlowResult:
""" """
Handle a single config flow step: validate input, create or update entries, or render the form. Handle a single config flow step: validate input, create or update entries, or render the form.
If valid credentials are provided, this will create a new config entry (title set to the provided email) or update an existing entry and trigger a reload. The step will abort if the validated account conflicts with an existing entry's unique ID. If no input is provided or validation fails, the flow returns a form populated with the given schema, any suggested values, and validation errors. If valid credentials are provided, this will create a new config entry (title set to the provided email) or update an existing entry and trigger a reload. The step will abort if the validated account conflicts with an existing entry's unique ID. If no input is provided or validation fails, the flow returns a form populated with the given schema, any suggested values, and validation errors.
Parameters: Parameters:
step_id: Identifier of the flow step to render or process. step_id: Identifier of the flow step to render or process.
schema: Voluptuous schema used to build the form. schema: Voluptuous schema used to build the form.
user_input: Submitted values from the form; when present, used for validation and entry creation/update. user_input: Submitted values from the form; when present, used for validation and entry creation/update.
suggested_values: Values to pre-fill into the form schema when rendering. suggested_values: Values to pre-fill into the form schema when rendering.
Returns: Returns:
A ConfigFlowResult representing either a created entry, an update-and-reload abort, an abort due to a unique-id conflict, or a form to display with errors and suggested values. A ConfigFlowResult representing either a created entry, an update-and-reload abort, an abort due to a unique-id conflict, or a form to display with errors and suggested values.
""" """
@@ -143,12 +143,12 @@ class OasisDeviceConfigFlow(ConfigFlow, domain=DOMAIN):
async def validate_client(self, user_input: dict[str, Any]) -> dict[str, str]: async def validate_client(self, user_input: dict[str, Any]) -> dict[str, str]:
""" """
Validate provided credentials by attempting to authenticate with the Oasis API and retrieve the user's identity. Validate provided credentials by attempting to authenticate with the Oasis API and retrieve the user's identity.
Parameters: Parameters:
user_input (dict[str, Any]): Mutable credential mapping containing at least `email` and `password`. user_input (dict[str, Any]): Mutable credential mapping containing at least `email` and `password`.
On success, this mapping will be updated with `CONF_ACCESS_TOKEN` (the received access token) On success, this mapping will be updated with `CONF_ACCESS_TOKEN` (the received access token)
and the `password` key will be removed. and the `password` key will be removed.
Returns: Returns:
dict[str, str]: A mapping of form field names to error keys. Common keys: dict[str, str]: A mapping of form field names to error keys. Common keys:
- `"base": "invalid_auth"` when credentials are incorrect or connection refused. - `"base": "invalid_auth"` when credentials are incorrect or connection refused.
@@ -179,9 +179,9 @@ class OasisDeviceConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except HTTPStatusError as err: except HTTPStatusError as err:
errors["base"] = str(err) errors["base"] = str(err)
except Exception as ex: # pylint: disable=broad-except except Exception:
_LOGGER.error(ex) _LOGGER.exception("Error while attempting to validate client")
errors["base"] = "unknown" errors["base"] = "unknown"
finally: finally:
await client.async_close() await client.async_close()
return errors return errors

View File

@@ -32,7 +32,7 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
) -> None: ) -> None:
""" """
Create an OasisDeviceCoordinator that manages OasisDevice discovery and updates using cloud and MQTT clients. Create an OasisDeviceCoordinator that manages OasisDevice discovery and updates using cloud and MQTT clients.
Parameters: Parameters:
cloud_client (OasisCloudClient): Client for communicating with the Oasis cloud API and fetching device data. cloud_client (OasisCloudClient): Client for communicating with the Oasis cloud API and fetching device data.
mqtt_client (OasisMqttClient): Client for registering devices and coordinating MQTT-based readiness/status. mqtt_client (OasisMqttClient): Client for registering devices and coordinating MQTT-based readiness/status.
@@ -50,10 +50,10 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
async def _async_update_data(self) -> list[OasisDevice]: async def _async_update_data(self) -> list[OasisDevice]:
""" """
Fetch and assemble the current list of OasisDevice objects, reconcile removed devices in Home Assistant, register discovered devices with MQTT, and verify per-device readiness. Fetch and assemble the current list of OasisDevice objects, reconcile removed devices in Home Assistant, register discovered devices with MQTT, and verify per-device readiness.
Returns: Returns:
A list of OasisDevice instances representing devices currently available for the account. A list of OasisDevice instances representing devices currently available for the account.
Raises: Raises:
UpdateFailed: If no devices can be read after repeated attempts or an unexpected error persists past retry limits. UpdateFailed: If no devices can be read after repeated attempts or an unexpected error persists past retry limits.
""" """
@@ -117,7 +117,7 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
# Best-effort playlists # Best-effort playlists
try: try:
await self.cloud_client.async_get_playlists() await self.cloud_client.async_get_playlists()
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception("Error fetching playlists from cloud") _LOGGER.exception("Error fetching playlists from cloud")
any_success = False any_success = False
@@ -145,7 +145,7 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
any_success = True any_success = True
device.schedule_track_refresh() device.schedule_track_refresh()
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception( _LOGGER.exception(
"Error preparing Oasis device %s", device.serial_number "Error preparing Oasis device %s", device.serial_number
) )
@@ -171,4 +171,4 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]):
if devices != self.data: if devices != self.data:
self.last_updated = dt_util.now() self.last_updated = dt_util.now()
return devices return devices

View File

@@ -234,8 +234,8 @@ class OasisCloudClient:
raise raise
except UnauthenticatedError: except UnauthenticatedError:
raise raise
except Exception as ex: # noqa: BLE001 except Exception:
_LOGGER.exception("Error fetching track %s: %s", track_id, ex) _LOGGER.exception("Error fetching track %s: %s", track_id)
return None return None
async def async_get_tracks( async def async_get_tracks(

View File

@@ -136,7 +136,7 @@ class OasisHttpClient(OasisClientProtocol):
mac = await self._async_get(params={"GETMAC": ""}) mac = await self._async_get(params={"GETMAC": ""})
if isinstance(mac, str): if isinstance(mac, str):
return mac.strip() return mac.strip()
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception( _LOGGER.exception(
"Failed to get MAC address via HTTP for %s", device.serial_number "Failed to get MAC address via HTTP for %s", device.serial_number
) )

View File

@@ -44,9 +44,9 @@ class OasisMqttClient(OasisClientProtocol):
# MQTT connection state # MQTT connection state
""" """
Initialize internal state for the MQTT transport client. Initialize internal state for the MQTT transport client.
Sets up connection state, per-device registries and events, subscription bookkeeping, and a bounded pending command queue capped by MAX_PENDING_COMMANDS. Sets up connection state, per-device registries and events, subscription bookkeeping, and a bounded pending command queue capped by MAX_PENDING_COMMANDS.
Attributes: Attributes:
_client: Active aiomqtt client or None. _client: Active aiomqtt client or None.
_loop_task: Background MQTT loop task or None. _loop_task: Background MQTT loop task or None.
@@ -86,12 +86,12 @@ class OasisMqttClient(OasisClientProtocol):
def register_device(self, device: OasisDevice) -> None: def register_device(self, device: OasisDevice) -> None:
""" """
Register an OasisDevice so MQTT messages for its serial are routed to that device. Register an OasisDevice so MQTT messages for its serial are routed to that device.
Ensures the device has a serial_number (raises ValueError if not), stores the device in the client's registry, creates per-device asyncio.Events for first-status and MAC-address arrival, attaches this client to the device if it has no client, and schedules a subscription for the device's STATUS topics if the MQTT client is currently connected. Ensures the device has a serial_number (raises ValueError if not), stores the device in the client's registry, creates per-device asyncio.Events for first-status and MAC-address arrival, attaches this client to the device if it has no client, and schedules a subscription for the device's STATUS topics if the MQTT client is currently connected.
Parameters: Parameters:
device (OasisDevice): The device instance to register. device (OasisDevice): The device instance to register.
Raises: Raises:
ValueError: If `device.serial_number` is not set. ValueError: If `device.serial_number` is not set.
""" """
@@ -123,7 +123,7 @@ class OasisMqttClient(OasisClientProtocol):
def register_devices(self, devices: Iterable[OasisDevice]) -> None: def register_devices(self, devices: Iterable[OasisDevice]) -> None:
""" """
Register multiple OasisDevice instances with the client. Register multiple OasisDevice instances with the client.
Parameters: Parameters:
devices (Iterable[OasisDevice]): Iterable of devices to register. devices (Iterable[OasisDevice]): Iterable of devices to register.
""" """
@@ -133,9 +133,9 @@ class OasisMqttClient(OasisClientProtocol):
def unregister_device(self, device: OasisDevice) -> None: def unregister_device(self, device: OasisDevice) -> None:
""" """
Unregisters a device from MQTT routing and cleans up related per-device state. Unregisters a device from MQTT routing and cleans up related per-device state.
Removes the device's registration, first-status and MAC events. If there is an active MQTT client and the device's serial is currently subscribed, schedules an asynchronous unsubscription task. If the device has no serial_number, the call is a no-op. Removes the device's registration, first-status and MAC events. If there is an active MQTT client and the device's serial is currently subscribed, schedules an asynchronous unsubscription task. If the device has no serial_number, the call is a no-op.
Parameters: Parameters:
device (OasisDevice): The device to unregister; must have `serial_number` set. device (OasisDevice): The device to unregister; must have `serial_number` set.
""" """
@@ -161,7 +161,7 @@ class OasisMqttClient(OasisClientProtocol):
async def _subscribe_serial(self, serial: str) -> None: async def _subscribe_serial(self, serial: str) -> None:
""" """
Subscribe to the device's STATUS topic pattern and mark the device as subscribed. Subscribe to the device's STATUS topic pattern and mark the device as subscribed.
Subscribes to "<serial>/STATUS/#" with QoS 1 and records the subscription; does nothing if the MQTT client is not connected or the serial is already subscribed. Subscribes to "<serial>/STATUS/#" with QoS 1 and records the subscription; does nothing if the MQTT client is not connected or the serial is already subscribed.
""" """
if not self._client: if not self._client:
@@ -179,7 +179,7 @@ class OasisMqttClient(OasisClientProtocol):
async def _unsubscribe_serial(self, serial: str) -> None: async def _unsubscribe_serial(self, serial: str) -> None:
""" """
Unsubscribe from the device's STATUS topic and update subscription state. Unsubscribe from the device's STATUS topic and update subscription state.
If there is no active MQTT client or the serial is not currently subscribed, this is a no-op. If there is no active MQTT client or the serial is not currently subscribed, this is a no-op.
Parameters: Parameters:
serial (str): Device serial used to build the topic "<serial>/STATUS/#". serial (str): Device serial used to build the topic "<serial>/STATUS/#".
@@ -217,7 +217,7 @@ class OasisMqttClient(OasisClientProtocol):
async def stop(self) -> None: async def stop(self) -> None:
""" """
Stop the MQTT client and clean up resources. Stop the MQTT client and clean up resources.
Signals the background MQTT loop to stop, cancels the loop task, disconnects the MQTT client if connected, and clears any pending commands from the internal command queue. Signals the background MQTT loop to stop, cancels the loop task, disconnects the MQTT client if connected, and clears any pending commands from the internal command queue.
""" """
self._stop_event.set() self._stop_event.set()
@@ -250,17 +250,17 @@ class OasisMqttClient(OasisClientProtocol):
) -> bool: ) -> bool:
""" """
Block until the MQTT client is connected and the device has emitted at least one STATUS message. Block until the MQTT client is connected and the device has emitted at least one STATUS message.
If `request_status` is True, a status request is sent after the client is connected to prompt the device to report its state. If `request_status` is True, a status request is sent after the client is connected to prompt the device to report its state.
Parameters: Parameters:
device (OasisDevice): The device to wait for; must have `serial_number` set. device (OasisDevice): The device to wait for; must have `serial_number` set.
timeout (float): Maximum seconds to wait for connection and for the first STATUS message. timeout (float): Maximum seconds to wait for connection and for the first STATUS message.
request_status (bool): If True, issue a status refresh after connection to encourage a STATUS update. request_status (bool): If True, issue a status refresh after connection to encourage a STATUS update.
Returns: Returns:
bool: `True` if the device's first STATUS message was observed within the timeout, `False` otherwise. bool: `True` if the device's first STATUS message was observed within the timeout, `False` otherwise.
Raises: Raises:
RuntimeError: If the provided device does not have a `serial_number`. RuntimeError: If the provided device does not have a `serial_number`.
""" """
@@ -288,7 +288,7 @@ class OasisMqttClient(OasisClientProtocol):
try: try:
first_status_event.clear() first_status_event.clear()
await self.async_get_status(device) await self.async_get_status(device)
except Exception: except Exception: # noqa: BLE001
_LOGGER.debug( _LOGGER.debug(
"Could not request status for %s (not fully connected yet?)", "Could not request status for %s (not fully connected yet?)",
serial, serial,
@@ -309,15 +309,15 @@ class OasisMqttClient(OasisClientProtocol):
async def async_get_mac_address(self, device: OasisDevice) -> str | None: async def async_get_mac_address(self, device: OasisDevice) -> str | None:
""" """
Request a device's MAC address via an MQTT STATUS refresh and return it if available. Request a device's MAC address via an MQTT STATUS refresh and return it if available.
If the device already has a MAC address, it is returned immediately. Otherwise the function requests a status update (which causes the device to publish MAC_ADDRESS) and waits up to 3 seconds for the MAC to arrive. If the device already has a MAC address, it is returned immediately. Otherwise the function requests a status update (which causes the device to publish MAC_ADDRESS) and waits up to 3 seconds for the MAC to arrive.
Parameters: Parameters:
device (OasisDevice): The device whose MAC address will be requested. device (OasisDevice): The device whose MAC address will be requested.
Returns: Returns:
str | None: The device MAC address if obtained, `None` if the wait timed out and no MAC was received. str | None: The device MAC address if obtained, `None` if the wait timed out and no MAC was received.
Raises: Raises:
RuntimeError: If the provided device has no serial_number set. RuntimeError: If the provided device has no serial_number set.
""" """
@@ -347,7 +347,7 @@ class OasisMqttClient(OasisClientProtocol):
) -> None: ) -> None:
""" """
Set the device's automatic cleaning mode. Set the device's automatic cleaning mode.
Parameters: Parameters:
device (OasisDevice): Target Oasis device to send the command to. device (OasisDevice): Target Oasis device to send the command to.
auto_clean (bool): True to enable automatic cleaning, False to disable. auto_clean (bool): True to enable automatic cleaning, False to disable.
@@ -362,7 +362,7 @@ class OasisMqttClient(OasisClientProtocol):
) -> None: ) -> None:
""" """
Set the device's ball speed. Set the device's ball speed.
Parameters: Parameters:
device (OasisDevice): Target device. device (OasisDevice): Target device.
speed (int): Speed value to apply. speed (int): Speed value to apply.
@@ -380,9 +380,9 @@ class OasisMqttClient(OasisClientProtocol):
) -> None: ) -> None:
""" """
Send an LED configuration command to the device. Send an LED configuration command to the device.
If `brightness` is greater than zero, the device is woken before sending the command. If `brightness` is greater than zero, the device is woken before sending the command.
Parameters: Parameters:
device (OasisDevice): Target device (must have a serial number). device (OasisDevice): Target device (must have a serial number).
led_effect (str): LED effect identifier to apply. led_effect (str): LED effect identifier to apply.
@@ -396,7 +396,7 @@ class OasisMqttClient(OasisClientProtocol):
async def async_send_sleep_command(self, device: OasisDevice) -> None: async def async_send_sleep_command(self, device: OasisDevice) -> None:
""" """
Send the sleep command to the specified Oasis device. Send the sleep command to the specified Oasis device.
Parameters: Parameters:
device (OasisDevice): Target device; must have a valid serial_number. If the MQTT client is not connected, the command may be queued for delivery when a connection is available. device (OasisDevice): Target device; must have a valid serial_number. If the MQTT client is not connected, the command may be queued for delivery when a connection is available.
""" """
@@ -410,7 +410,7 @@ class OasisMqttClient(OasisClientProtocol):
) -> None: ) -> None:
""" """
Move a job in the device's playlist from one index to another. Move a job in the device's playlist from one index to another.
Parameters: Parameters:
device (OasisDevice): Target device to receive the command. device (OasisDevice): Target device to receive the command.
from_index (int): Source index of the job in the playlist. from_index (int): Source index of the job in the playlist.
@@ -426,10 +426,10 @@ class OasisMqttClient(OasisClientProtocol):
) -> None: ) -> None:
""" """
Change the device's current track to the specified track index. Change the device's current track to the specified track index.
Parameters: Parameters:
device (OasisDevice): Target Oasis device. device (OasisDevice): Target Oasis device.
index (int): Track index to switch to (zero-based). index (int): Track index to switch to (zero-based).
""" """
payload = f"CMDCHANGETRACK={index}" payload = f"CMDCHANGETRACK={index}"
await self._publish_command(device, payload) await self._publish_command(device, payload)
@@ -441,7 +441,7 @@ class OasisMqttClient(OasisClientProtocol):
) -> None: ) -> None:
""" """
Send an ADDJOBLIST command to add multiple tracks to the device's job list. Send an ADDJOBLIST command to add multiple tracks to the device's job list.
Parameters: Parameters:
device (OasisDevice): Target device to receive the command. device (OasisDevice): Target device to receive the command.
tracks (list[int]): List of track indices to add; elements will be joined as a comma-separated list in the command payload. tracks (list[int]): List of track indices to add; elements will be joined as a comma-separated list in the command payload.
@@ -457,7 +457,7 @@ class OasisMqttClient(OasisClientProtocol):
) -> None: ) -> None:
""" """
Set the device's playlist to the specified ordered list of track indices. Set the device's playlist to the specified ordered list of track indices.
Parameters: Parameters:
device (OasisDevice): Target Oasis device to receive the playlist command. device (OasisDevice): Target Oasis device to receive the playlist command.
playlist (list[int]): Ordered list of track indices to apply as the device's playlist. playlist (list[int]): Ordered list of track indices to apply as the device's playlist.
@@ -473,7 +473,7 @@ class OasisMqttClient(OasisClientProtocol):
) -> None: ) -> None:
""" """
Send a command to enable or disable repeating the device's playlist. Send a command to enable or disable repeating the device's playlist.
Parameters: Parameters:
device (OasisDevice): Target device; must have a serial number. device (OasisDevice): Target device; must have a serial number.
repeat (bool): True to enable playlist repeat, False to disable it. repeat (bool): True to enable playlist repeat, False to disable it.
@@ -488,9 +488,9 @@ class OasisMqttClient(OasisClientProtocol):
) -> None: ) -> None:
""" """
Set the device's wait-after-job / autoplay option. Set the device's wait-after-job / autoplay option.
Publishes a "WRIWAITAFTER=<option>" command for the specified device to configure how long the device waits after a job or to adjust autoplay behavior. Publishes a "WRIWAITAFTER=<option>" command for the specified device to configure how long the device waits after a job or to adjust autoplay behavior.
Parameters: Parameters:
device (OasisDevice): Target device (must have a serial_number). device (OasisDevice): Target device (must have a serial_number).
option (str): Value accepted by the device firmware for the wait-after-job/autoplay setting (typically a numeric string or predefined option token). option (str): Value accepted by the device firmware for the wait-after-job/autoplay setting (typically a numeric string or predefined option token).
@@ -505,9 +505,9 @@ class OasisMqttClient(OasisClientProtocol):
) -> None: ) -> None:
""" """
Request a firmware upgrade for the given device. Request a firmware upgrade for the given device.
Sends an upgrade command to the device and selects the beta channel when requested. Sends an upgrade command to the device and selects the beta channel when requested.
Parameters: Parameters:
device (OasisDevice): Target device. device (OasisDevice): Target device.
beta (bool): If `True`, request a beta firmware upgrade; if `False`, request the stable firmware. beta (bool): If `True`, request a beta firmware upgrade; if `False`, request the stable firmware.
@@ -524,7 +524,7 @@ class OasisMqttClient(OasisClientProtocol):
async def async_send_pause_command(self, device: OasisDevice) -> None: async def async_send_pause_command(self, device: OasisDevice) -> None:
""" """
Sends a pause command to the specified Oasis device. Sends a pause command to the specified Oasis device.
Publishes the "CMDPAUSE" command to the device's command topic. Publishes the "CMDPAUSE" command to the device's command topic.
""" """
await self._publish_command(device, "CMDPAUSE") await self._publish_command(device, "CMDPAUSE")
@@ -532,7 +532,7 @@ class OasisMqttClient(OasisClientProtocol):
async def async_send_stop_command(self, device: OasisDevice) -> None: async def async_send_stop_command(self, device: OasisDevice) -> None:
""" """
Send the "stop" command to the given Oasis device. Send the "stop" command to the given Oasis device.
Parameters: Parameters:
device (OasisDevice): Target device to receive the stop command; must be registered with a valid serial number. device (OasisDevice): Target device to receive the stop command; must be registered with a valid serial number.
""" """
@@ -541,7 +541,7 @@ class OasisMqttClient(OasisClientProtocol):
async def async_send_reboot_command(self, device: OasisDevice) -> None: async def async_send_reboot_command(self, device: OasisDevice) -> None:
""" """
Send a reboot command to the specified Oasis device. Send a reboot command to the specified Oasis device.
Parameters: Parameters:
device (OasisDevice): Target device to receive the reboot command; must have a valid serial_number. device (OasisDevice): Target device to receive the reboot command; must have a valid serial_number.
""" """
@@ -579,7 +579,7 @@ class OasisMqttClient(OasisClientProtocol):
async def _flush_pending_commands(self) -> None: async def _flush_pending_commands(self) -> None:
""" """
Flush queued commands by publishing them to each device's COMMAND/CMD topic. Flush queued commands by publishing them to each device's COMMAND/CMD topic.
This consumes all entries from the internal command queue, skipping entries for devices that are no longer registered, publishing each payload to "<serial>/COMMAND/CMD" with QoS 1, and marking queue tasks done. If a publish fails, the failed command is re-queued and flushing stops so remaining queued commands will be retried on the next reconnect. This consumes all entries from the internal command queue, skipping entries for devices that are no longer registered, publishing each payload to "<serial>/COMMAND/CMD" with QoS 1, and marking queue tasks done. If a publish fails, the failed command is re-queued and flushing stops so remaining queued commands will be retried on the next reconnect.
""" """
if not self._client: if not self._client:
@@ -605,7 +605,7 @@ class OasisMqttClient(OasisClientProtocol):
topic = f"{serial}/COMMAND/CMD" topic = f"{serial}/COMMAND/CMD"
_LOGGER.debug("Flushing queued MQTT command %s => %s", topic, payload) _LOGGER.debug("Flushing queued MQTT command %s => %s", topic, payload)
await self._client.publish(topic, payload.encode(), qos=1) await self._client.publish(topic, payload.encode(), qos=1)
except Exception: except Exception: # noqa: BLE001
_LOGGER.debug( _LOGGER.debug(
"Failed to flush queued command for %s, re-queuing", serial "Failed to flush queued command for %s, re-queuing", serial
) )
@@ -621,14 +621,14 @@ class OasisMqttClient(OasisClientProtocol):
) -> None: ) -> None:
""" """
Publish a command payload to the device's MQTT COMMAND topic, queueing it if the client is not connected. Publish a command payload to the device's MQTT COMMAND topic, queueing it if the client is not connected.
If `wake` is True and the device reports it is sleeping, requests a full status refresh before publishing. If the MQTT client is not connected or publish fails, the command is enqueued for later delivery. If `wake` is True and the device reports it is sleeping, requests a full status refresh before publishing. If the MQTT client is not connected or publish fails, the command is enqueued for later delivery.
Parameters: Parameters:
device (OasisDevice): Target device; must have a valid `serial_number`. device (OasisDevice): Target device; must have a valid `serial_number`.
payload (str): Command payload to send to the device. payload (str): Command payload to send to the device.
wake (bool): If True, refresh the device state when the device is sleeping before sending the command. wake (bool): If True, refresh the device state when the device is sleeping before sending the command.
Raises: Raises:
RuntimeError: If the provided device has no serial number set. RuntimeError: If the provided device has no serial number set.
""" """
@@ -651,7 +651,7 @@ class OasisMqttClient(OasisClientProtocol):
try: try:
_LOGGER.debug("MQTT publish %s => %s", topic, payload) _LOGGER.debug("MQTT publish %s => %s", topic, payload)
await self._client.publish(topic, payload.encode(), qos=1) await self._client.publish(topic, payload.encode(), qos=1)
except Exception: except Exception: # noqa: BLE001
_LOGGER.debug( _LOGGER.debug(
"MQTT publish failed, queueing command for %s: %s", serial, payload "MQTT publish failed, queueing command for %s: %s", serial, payload
) )
@@ -660,7 +660,7 @@ class OasisMqttClient(OasisClientProtocol):
async def _mqtt_loop(self) -> None: async def _mqtt_loop(self) -> None:
""" """
Run the MQTT WebSocket connection loop that maintains connection, subscriptions, and message handling. Run the MQTT WebSocket connection loop that maintains connection, subscriptions, and message handling.
This background coroutine establishes a persistent WSS MQTT connection to the configured broker, sets connection state on successful connect, resubscribes to known device STATUS topics, flushes any queued outbound commands, and dispatches incoming MQTT messages to the status handler. On disconnect or error it clears connection state and subscription tracking, and retries connecting after the configured backoff interval until the client is stopped. This background coroutine establishes a persistent WSS MQTT connection to the configured broker, sets connection state on successful connect, resubscribes to known device STATUS topics, flushes any queued outbound commands, and dispatches incoming MQTT messages to the status handler. On disconnect or error it clears connection state and subscription tracking, and retries connecting after the configured backoff interval until the client is stopped.
""" """
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
@@ -698,7 +698,7 @@ class OasisMqttClient(OasisClientProtocol):
except asyncio.CancelledError: except asyncio.CancelledError:
break break
except Exception: except Exception: # noqa: BLE001
_LOGGER.info("MQTT connection error") _LOGGER.info("MQTT connection error")
finally: finally:
@@ -722,13 +722,13 @@ class OasisMqttClient(OasisClientProtocol):
async def _handle_status_message(self, msg: aiomqtt.Message) -> None: async def _handle_status_message(self, msg: aiomqtt.Message) -> None:
""" """
Map an incoming MQTT STATUS message to an OasisDevice state update. Map an incoming MQTT STATUS message to an OasisDevice state update.
Expects msg.topic in the form "<serial>/STATUS/<STATUS_NAME>" and decodes msg.payload as text. Expects msg.topic in the form "<serial>/STATUS/<STATUS_NAME>" and decodes msg.payload as text.
If the topic corresponds to a registered device, extracts the relevant status field and calls If the topic corresponds to a registered device, extracts the relevant status field and calls
the device's update_from_status_dict with a mapping of the parsed values. For the "MAC_ADDRESS" the device's update_from_status_dict with a mapping of the parsed values. For the "MAC_ADDRESS"
status, sets the per-device MAC event to signal arrival of the MAC address. Always sets the status, sets the per-device MAC event to signal arrival of the MAC address. Always sets the
per-device first-status event once any status is processed for that serial. per-device first-status event once any status is processed for that serial.
Parameters: Parameters:
msg (aiomqtt.Message): Incoming MQTT message; topic identifies device serial and status. msg (aiomqtt.Message): Incoming MQTT message; topic identifies device serial and status.
""" """
@@ -818,7 +818,7 @@ class OasisMqttClient(OasisClientProtocol):
status_name, status_name,
payload, payload,
) )
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception( _LOGGER.exception(
"Error parsing MQTT payload for %s %s: %r", serial, status_name, payload "Error parsing MQTT payload for %s %s: %r", serial, status_name, payload
) )
@@ -831,4 +831,4 @@ class OasisMqttClient(OasisClientProtocol):
serial, asyncio.Event() serial, asyncio.Event()
) )
if not first_status_event.is_set(): if not first_status_event.is_set():
first_status_event.set() first_status_event.set()

View File

@@ -305,7 +305,7 @@ class OasisDevice:
if n > 18: if n > 18:
status["software_version"] = values[18] status["software_version"] = values[18]
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception( _LOGGER.exception(
"Error parsing status string for %s: %r", self.serial_number, raw_status "Error parsing status string for %s: %r", self.serial_number, raw_status
) )
@@ -487,7 +487,7 @@ class OasisDevice:
for listener in list(self._listeners): for listener in list(self._listeners):
try: try:
listener() listener()
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception("Error in update listener") _LOGGER.exception("Error in update listener")
async def async_get_mac_address(self) -> str | None: async def async_get_mac_address(self) -> str | None:
@@ -751,7 +751,7 @@ class OasisDevice:
try: try:
track = await self._cloud.async_get_track_info(track_id) track = await self._cloud.async_get_track_info(track_id)
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception("Error fetching track info for %s", track_id) _LOGGER.exception("Error fetching track info for %s", track_id)
return return

View File

@@ -30,29 +30,29 @@ def _bit_to_bool(val: str) -> bool:
def _parse_int(val: str) -> int: def _parse_int(val: str) -> int:
""" """
Parse a string into an integer, falling back to 0 when conversion fails. Parse a string into an integer, falling back to 0 when conversion fails.
Parameters: Parameters:
val (str): String potentially containing an integer value. val (str): String potentially containing an integer value.
Returns: Returns:
int: The parsed integer, or 0 if `val` cannot be converted. int: The parsed integer, or 0 if `val` cannot be converted.
""" """
try: try:
return int(val) return int(val)
except Exception: except Exception: # noqa: BLE001
return 0 return 0
def create_svg(track: dict, progress: int) -> str | None: def create_svg(track: dict, progress: int) -> str | None:
""" """
Create an SVG visualization of a track showing progress as a completed path and indicator. Create an SVG visualization of a track showing progress as a completed path and indicator.
Builds an SVG representation from the track's "svg_content" and the provided progress value. If progress is supplied, the function will decrypt the stored SVG content (if needed), compute which path segments are complete using the track's optional "reduced_svg_content_new" value or the number of path segments, and render a base arc, completed arc, track, completed track segment, background circle, and a ball indicator positioned at the current progress point. Returns None if input is missing or an error occurs. Builds an SVG representation from the track's "svg_content" and the provided progress value. If progress is supplied, the function will decrypt the stored SVG content (if needed), compute which path segments are complete using the track's optional "reduced_svg_content_new" value or the number of path segments, and render a base arc, completed arc, track, completed track segment, background circle, and a ball indicator positioned at the current progress point. Returns None if input is missing or an error occurs.
Parameters: Parameters:
track (dict): Track data containing at minimum an "svg_content" entry and optionally "reduced_svg_content_new" to indicate total segments. track (dict): Track data containing at minimum an "svg_content" entry and optionally "reduced_svg_content_new" to indicate total segments.
progress (int): Current progress expressed as a count relative to the track's total segments. progress (int): Current progress expressed as a count relative to the track's total segments.
Returns: Returns:
str | None: Serialized SVG markup as a UTF-8 string when successful, otherwise `None`. str | None: Serialized SVG markup as a UTF-8 string when successful, otherwise `None`.
""" """
@@ -169,8 +169,8 @@ def create_svg(track: dict, progress: int) -> str | None:
) )
return tostring(svg).decode() return tostring(svg).decode()
except Exception as e: except Exception:
_LOGGER.exception(e) _LOGGER.exception("Error creating svg")
return None return None
@@ -200,4 +200,4 @@ def decrypt_svg_content(svg_content: dict[str, str]):
def now() -> datetime: def now() -> datetime:
return datetime.now(UTC) return datetime.now(UTC)