From e4f6cd2803a9783f87dd4bcc46ba0ac725829c67 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Mon, 24 Nov 2025 04:06:42 +0000 Subject: [PATCH] Adjust exceptions --- custom_components/oasis_mini/__init__.py | 43 ++++---- custom_components/oasis_mini/config_flow.py | 28 ++--- custom_components/oasis_mini/coordinator.py | 12 +-- .../pyoasiscontrol/clients/cloud_client.py | 4 +- .../pyoasiscontrol/clients/http_client.py | 2 +- .../pyoasiscontrol/clients/mqtt_client.py | 102 +++++++++--------- .../oasis_mini/pyoasiscontrol/device.py | 6 +- .../oasis_mini/pyoasiscontrol/utils.py | 18 ++-- 8 files changed, 106 insertions(+), 109 deletions(-) diff --git a/custom_components/oasis_mini/__init__.py b/custom_components/oasis_mini/__init__.py index 2c8a42a..d8faa41 100644 --- a/custom_components/oasis_mini/__init__.py +++ b/custom_components/oasis_mini/__init__.py @@ -46,9 +46,9 @@ def setup_platform_from_coordinator( ) -> None: """ 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. - + Parameters: entry: Config entry containing the coordinator in its `runtime_data`. 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: """ 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 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 @@ -95,7 +95,7 @@ def setup_platform_from_coordinator( 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. - + Returns: 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) - try: - await coordinator.async_config_entry_first_refresh() - except Exception as ex: - _LOGGER.exception(ex) + await coordinator.async_config_entry_first_refresh() if entry.unique_id != (user_id := str(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: """ 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. """ coordinator.last_updated = dt_util.now() coordinator.async_update_listeners() - for device in coordinator.data: + for device in coordinator.data or []: device.add_update_listener(_on_oasis_update) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -145,9 +142,9 @@ async def async_unload_entry( ) -> bool: """ Cleanly unload an Oasis device config entry. - + Closes the MQTT and cloud clients stored on the entry and unloads all supported platforms. - + Returns: `True` if all platforms were unloaded successfully, `False` otherwise. """ @@ -165,29 +162,29 @@ async def async_remove_entry( ) -> None: """ 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. """ cloud_client = create_client(hass, entry.data) try: await cloud_client.async_logout() - except Exception as ex: - _LOGGER.exception(ex) + except Exception: + _LOGGER.exception("Error attempting to logout from the cloud") await cloud_client.async_close() async def async_migrate_entry(hass: HomeAssistant, entry: OasisDeviceConfigEntry): """ Migrate an Oasis config entry to the current schema (minor version 3). - + Performs in-place migrations for older entries: - 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. - Updates the config entry's data, options, minor_version, title (from CONF_EMAIL or "Oasis Control"), unique_id, and version. - + Parameters: entry: The config entry to migrate. - + Returns: `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: """ Update a registry entry's unique_id suffix from "-playlist" to "-queue" when applicable. - + Parameters: entity_entry (er.RegistryEntry): Registry entry to inspect. - + Returns: dict[str, Any] | None: A mapping {"new_unique_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: """ Determine whether the config entry is no longer associated with the given device. - + Parameters: 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. - + Returns: 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 for identifier in device_entry.identifiers if identifier[0] == DOMAIN and identifier[1] in current_serials - ) \ No newline at end of file + ) diff --git a/custom_components/oasis_mini/config_flow.py b/custom_components/oasis_mini/config_flow.py index 9a9b3bc..e1427ff 100644 --- a/custom_components/oasis_mini/config_flow.py +++ b/custom_components/oasis_mini/config_flow.py @@ -35,10 +35,10 @@ class OasisDeviceConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """ Begin the reauthentication flow for an existing config entry. - + Parameters: entry_data (Mapping[str, Any]): Data from the existing config entry that triggered the reauthentication flow. - + Returns: ConfigFlowResult: Result that presents the reauthentication confirmation dialog to the user. """ @@ -49,9 +49,9 @@ class OasisDeviceConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """ 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. - + Returns: 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: """ Handle the initial user configuration step for the Oasis integration. - + Parameters: user_input (dict[str, Any] | None): Optional prefilled values (e.g., `email`, `password`) submitted by the user. - + Returns: 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: """ 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. - + Parameters: step_id: Identifier of the flow step to render or process. schema: Voluptuous schema used to build the form. 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. - + 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. """ @@ -143,12 +143,12 @@ class OasisDeviceConfigFlow(ConfigFlow, domain=DOMAIN): 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. - + Parameters: 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) and the `password` key will be removed. - + Returns: dict[str, str]: A mapping of form field names to error keys. Common keys: - `"base": "invalid_auth"` when credentials are incorrect or connection refused. @@ -179,9 +179,9 @@ class OasisDeviceConfigFlow(ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" except HTTPStatusError as err: errors["base"] = str(err) - except Exception as ex: # pylint: disable=broad-except - _LOGGER.error(ex) + except Exception: + _LOGGER.exception("Error while attempting to validate client") errors["base"] = "unknown" finally: await client.async_close() - return errors \ No newline at end of file + return errors diff --git a/custom_components/oasis_mini/coordinator.py b/custom_components/oasis_mini/coordinator.py index 41a60a2..4261d42 100644 --- a/custom_components/oasis_mini/coordinator.py +++ b/custom_components/oasis_mini/coordinator.py @@ -32,7 +32,7 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]): ) -> None: """ Create an OasisDeviceCoordinator that manages OasisDevice discovery and updates using cloud and MQTT clients. - + Parameters: 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. @@ -50,10 +50,10 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[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. - + Returns: A list of OasisDevice instances representing devices currently available for the account. - + Raises: 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 try: await self.cloud_client.async_get_playlists() - except Exception: # noqa: BLE001 + except Exception: _LOGGER.exception("Error fetching playlists from cloud") any_success = False @@ -145,7 +145,7 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]): any_success = True device.schedule_track_refresh() - except Exception: # noqa: BLE001 + except Exception: _LOGGER.exception( "Error preparing Oasis device %s", device.serial_number ) @@ -171,4 +171,4 @@ class OasisDeviceCoordinator(DataUpdateCoordinator[list[OasisDevice]]): if devices != self.data: self.last_updated = dt_util.now() - return devices \ No newline at end of file + return devices diff --git a/custom_components/oasis_mini/pyoasiscontrol/clients/cloud_client.py b/custom_components/oasis_mini/pyoasiscontrol/clients/cloud_client.py index dd2bf5a..e45992d 100644 --- a/custom_components/oasis_mini/pyoasiscontrol/clients/cloud_client.py +++ b/custom_components/oasis_mini/pyoasiscontrol/clients/cloud_client.py @@ -234,8 +234,8 @@ class OasisCloudClient: raise except UnauthenticatedError: raise - except Exception as ex: # noqa: BLE001 - _LOGGER.exception("Error fetching track %s: %s", track_id, ex) + except Exception: + _LOGGER.exception("Error fetching track %s: %s", track_id) return None async def async_get_tracks( diff --git a/custom_components/oasis_mini/pyoasiscontrol/clients/http_client.py b/custom_components/oasis_mini/pyoasiscontrol/clients/http_client.py index 88ccfac..f1cb08a 100644 --- a/custom_components/oasis_mini/pyoasiscontrol/clients/http_client.py +++ b/custom_components/oasis_mini/pyoasiscontrol/clients/http_client.py @@ -136,7 +136,7 @@ class OasisHttpClient(OasisClientProtocol): mac = await self._async_get(params={"GETMAC": ""}) if isinstance(mac, str): return mac.strip() - except Exception: # noqa: BLE001 + except Exception: _LOGGER.exception( "Failed to get MAC address via HTTP for %s", device.serial_number ) diff --git a/custom_components/oasis_mini/pyoasiscontrol/clients/mqtt_client.py b/custom_components/oasis_mini/pyoasiscontrol/clients/mqtt_client.py index de6eec1..f050f63 100644 --- a/custom_components/oasis_mini/pyoasiscontrol/clients/mqtt_client.py +++ b/custom_components/oasis_mini/pyoasiscontrol/clients/mqtt_client.py @@ -44,9 +44,9 @@ class OasisMqttClient(OasisClientProtocol): # MQTT connection state """ 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. - + Attributes: _client: Active aiomqtt client or None. _loop_task: Background MQTT loop task or None. @@ -86,12 +86,12 @@ class OasisMqttClient(OasisClientProtocol): def register_device(self, device: OasisDevice) -> None: """ 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. - + Parameters: device (OasisDevice): The device instance to register. - + Raises: ValueError: If `device.serial_number` is not set. """ @@ -123,7 +123,7 @@ class OasisMqttClient(OasisClientProtocol): def register_devices(self, devices: Iterable[OasisDevice]) -> None: """ Register multiple OasisDevice instances with the client. - + Parameters: devices (Iterable[OasisDevice]): Iterable of devices to register. """ @@ -133,9 +133,9 @@ class OasisMqttClient(OasisClientProtocol): def unregister_device(self, device: OasisDevice) -> None: """ 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. - + Parameters: 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: """ Subscribe to the device's STATUS topic pattern and mark the device as subscribed. - + Subscribes to "/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: @@ -179,7 +179,7 @@ class OasisMqttClient(OasisClientProtocol): async def _unsubscribe_serial(self, serial: str) -> None: """ 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. Parameters: serial (str): Device serial used to build the topic "/STATUS/#". @@ -217,7 +217,7 @@ class OasisMqttClient(OasisClientProtocol): async def stop(self) -> None: """ 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. """ self._stop_event.set() @@ -250,17 +250,17 @@ class OasisMqttClient(OasisClientProtocol): ) -> bool: """ 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. - + Parameters: 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. request_status (bool): If True, issue a status refresh after connection to encourage a STATUS update. - + Returns: bool: `True` if the device's first STATUS message was observed within the timeout, `False` otherwise. - + Raises: RuntimeError: If the provided device does not have a `serial_number`. """ @@ -288,7 +288,7 @@ class OasisMqttClient(OasisClientProtocol): try: first_status_event.clear() await self.async_get_status(device) - except Exception: + except Exception: # noqa: BLE001 _LOGGER.debug( "Could not request status for %s (not fully connected yet?)", serial, @@ -309,15 +309,15 @@ class OasisMqttClient(OasisClientProtocol): 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. - + 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: device (OasisDevice): The device whose MAC address will be requested. - + Returns: str | None: The device MAC address if obtained, `None` if the wait timed out and no MAC was received. - + Raises: RuntimeError: If the provided device has no serial_number set. """ @@ -347,7 +347,7 @@ class OasisMqttClient(OasisClientProtocol): ) -> None: """ Set the device's automatic cleaning mode. - + Parameters: device (OasisDevice): Target Oasis device to send the command to. auto_clean (bool): True to enable automatic cleaning, False to disable. @@ -362,7 +362,7 @@ class OasisMqttClient(OasisClientProtocol): ) -> None: """ Set the device's ball speed. - + Parameters: device (OasisDevice): Target device. speed (int): Speed value to apply. @@ -380,9 +380,9 @@ class OasisMqttClient(OasisClientProtocol): ) -> None: """ Send an LED configuration command to the device. - + If `brightness` is greater than zero, the device is woken before sending the command. - + Parameters: device (OasisDevice): Target device (must have a serial number). 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: """ Send the sleep command to the specified Oasis device. - + 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. """ @@ -410,7 +410,7 @@ class OasisMqttClient(OasisClientProtocol): ) -> None: """ Move a job in the device's playlist from one index to another. - + Parameters: device (OasisDevice): Target device to receive the command. from_index (int): Source index of the job in the playlist. @@ -426,10 +426,10 @@ class OasisMqttClient(OasisClientProtocol): ) -> None: """ Change the device's current track to the specified track index. - + Parameters: - device (OasisDevice): Target Oasis device. - index (int): Track index to switch to (zero-based). + device (OasisDevice): Target Oasis device. + index (int): Track index to switch to (zero-based). """ payload = f"CMDCHANGETRACK={index}" await self._publish_command(device, payload) @@ -441,7 +441,7 @@ class OasisMqttClient(OasisClientProtocol): ) -> None: """ Send an ADDJOBLIST command to add multiple tracks to the device's job list. - + Parameters: 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. @@ -457,7 +457,7 @@ class OasisMqttClient(OasisClientProtocol): ) -> None: """ Set the device's playlist to the specified ordered list of track indices. - + Parameters: 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. @@ -473,7 +473,7 @@ class OasisMqttClient(OasisClientProtocol): ) -> None: """ Send a command to enable or disable repeating the device's playlist. - + Parameters: device (OasisDevice): Target device; must have a serial number. repeat (bool): True to enable playlist repeat, False to disable it. @@ -488,9 +488,9 @@ class OasisMqttClient(OasisClientProtocol): ) -> None: """ Set the device's wait-after-job / autoplay option. - + Publishes a "WRIWAITAFTER=