From 4489632aa8cdc904d347f60e99499b4bc2fc4010 Mon Sep 17 00:00:00 2001 From: partach Date: Thu, 30 Jul 2026 18:14:15 +0000 Subject: [PATCH] Revert ruff refactor in custom_components; pin lint rule set The ruff cleanup rewrote ~90 except clauses across every protocol client and coordinator. Restore custom_components/ to the last state where Modbus reads were confirmed working (511c8c7, which keeps the FC 06 single-register write fix). Add ruff.toml pinning the rule set to ruff's documented defaults (E4/E7/E9/F) so lint results are deterministic across ruff versions and don't push broad-except narrowing into protocol code, where catching everything is intentional. https://claude.ai/code/session_0141XkQRjcng4RKLD4Yv5FAr --- custom_components/protocol_wizard/__init__.py | 92 ++++++++---------- .../protocol_wizard/config_flow.py | 97 ++++++++----------- .../protocol_wizard/entity_base.py | 48 ++++----- custom_components/protocol_wizard/number.py | 9 +- .../protocol_wizard/options_flow.py | 39 ++++---- .../protocol_wizard/protocols/__init__.py | 12 +-- .../protocols/bacnet/__init__.py | 14 +-- .../protocols/bacnet/client.py | 66 ++++++------- .../protocols/bacnet/coordinator.py | 27 +++--- .../protocol_wizard/protocols/base.py | 20 +++- .../protocols/modbus/__init__.py | 4 +- .../protocols/modbus/client.py | 16 +-- .../protocols/modbus/coordinator.py | 45 ++++----- .../protocols/mqtt/__init__.py | 18 ++-- .../protocol_wizard/protocols/mqtt/client.py | 14 +-- .../protocols/mqtt/coordinator.py | 22 ++--- .../protocols/snmp/__init__.py | 4 +- .../protocol_wizard/protocols/snmp/client.py | 16 +-- .../protocols/snmp/coordinator.py | 21 ++-- custom_components/protocol_wizard/select.py | 9 +- custom_components/protocol_wizard/sensor.py | 11 +-- custom_components/protocol_wizard/switch.py | 9 +- .../protocol_wizard/template_utils.py | 27 ++---- ruff.toml | 10 ++ 24 files changed, 305 insertions(+), 345 deletions(-) create mode 100644 ruff.toml diff --git a/custom_components/protocol_wizard/__init__.py b/custom_components/protocol_wizard/__init__.py index ccd213a..1b43408 100644 --- a/custom_components/protocol_wizard/__init__.py +++ b/custom_components/protocol_wizard/__init__.py @@ -2,70 +2,64 @@ #-- base init.py protocol wizard #------------------------------------------ """The Protocol Wizard integration.""" -import asyncio +import shutil import logging import os +import asyncio import re -import shutil -from datetime import timedelta +from homeassistant.helpers import device_registry as dr, entity_registry as er, config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, ServiceCall +from pymodbus.client import AsyncModbusSerialClient, AsyncModbusTcpClient, AsyncModbusUdpClient from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.service import SupportsResponse -from pymodbus.client import ( - AsyncModbusSerialClient, - AsyncModbusTcpClient, - AsyncModbusUdpClient, -) +from datetime import timedelta +# Import protocol registry and plugins +from .protocols import ProtocolRegistry +from .protocols.modbus import ModbusClient +from .protocols.snmp import SNMPClient +from .protocols.mqtt import MQTTClient +from .protocols.bacnet.client import BACnetClient +from .template_utils import ensure_user_template_dirs, load_template from .const import ( - CONF_BACNET_DEVICES, CONF_BAUDRATE, CONF_BYTESIZE, CONF_CONNECTION_TYPE, - CONF_ENTITIES, CONF_HOST, - CONF_NAME, CONF_PARITY, CONF_PORT, - CONF_PROTOCOL, - CONF_PROTOCOL_BACNET, - CONF_PROTOCOL_MODBUS, - CONF_PROTOCOL_MQTT, - CONF_PROTOCOL_SNMP, - CONF_REGISTERS, CONF_SERIAL_PORT, CONF_SLAVE_ID, - CONF_SLAVES, CONF_STOPBITS, - CONF_TEMPLATE, - CONF_TEMPLATE_APPLIED, CONF_UPDATE_INTERVAL, - CONNECTION_TYPE_IP, + CONF_NAME, CONNECTION_TYPE_SERIAL, - CONNECTION_TYPE_TCP, + CONNECTION_TYPE_IP, CONNECTION_TYPE_UDP, + CONNECTION_TYPE_TCP, DEFAULT_BAUDRATE, DEFAULT_BYTESIZE, DEFAULT_PARITY, DEFAULT_STOPBITS, DOMAIN, + CONF_PROTOCOL_MODBUS, + CONF_PROTOCOL_SNMP, + CONF_PROTOCOL_MQTT, + CONF_PROTOCOL_BACNET, + CONF_PROTOCOL, + CONF_TEMPLATE, + CONF_TEMPLATE_APPLIED, + CONF_ENTITIES, + CONF_REGISTERS, + CONF_SLAVES, + CONF_BACNET_DEVICES, SIGNAL_ENTITY_SYNC, ) -# Import protocol registry and plugins -from .protocols import ProtocolRegistry -from .protocols.bacnet.client import BACnetClient -from .protocols.modbus import ModbusClient -from .protocols.mqtt import MQTTClient -from .protocols.snmp import SNMPClient -from .template_utils import ensure_user_template_dirs, load_template _LOGGER = logging.getLogger(__name__) @@ -93,7 +87,7 @@ def install(): else: _LOGGER.warning("Frontend source file missing at %s", source_path) - except OSError as err: + except Exception as err: _LOGGER.error("Failed to install frontend resource: %s", err) await hass.async_add_executor_job(install) @@ -428,7 +422,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: _LOGGER.error("Protocol %s not yet implemented", protocol_name) return False - except (OSError, ConnectionError, TimeoutError, ValueError) as err: + except Exception as err: _LOGGER.error("Failed to create client for %s: %s", protocol_name, err) return False @@ -545,7 +539,7 @@ async def _load_template_into_options( hass.config_entries.async_update_entry(entry, options=new_options) - except (OSError, KeyError, TypeError, ValueError) as err: + except Exception as err: _LOGGER.error("Failed to load template %s: %s", template_name, err) @@ -619,13 +613,7 @@ def _create_snmp_client(config: dict) -> SNMPClient: def _create_mqtt_client(config: dict) -> MQTTClient: """Create MQTT client (no caching needed - manages its own connection).""" - from .protocols.mqtt import ( - CONF_BROKER, - CONF_PASSWORD, - CONF_USERNAME, - DEFAULT_PORT, - MQTTClient, - ) + from .protocols.mqtt import MQTTClient, CONF_BROKER, CONF_USERNAME, CONF_PASSWORD, DEFAULT_PORT return MQTTClient( broker=config[CONF_BROKER], @@ -816,7 +804,7 @@ async def handle_add_entity(call: ServiceCall): # Add optional fields if provided for field in ["format", "options", "device_class", "state_class", "entity_category", "icon", "min", "max", "step"]: - if call.data.get(field): + if field in call.data and call.data[field]: new_entity[field] = call.data[field] # Check for duplicates @@ -880,8 +868,8 @@ async def handle_add_entity(call: ServiceCall): } except Exception as err: - _LOGGER.exception("Failed to add entity") - raise HomeAssistantError(f"Failed to add entity: {err!s}") from err + _LOGGER.error("Failed to add entity: %s", err, exc_info=True) + raise HomeAssistantError(f"Failed to add entity: {str(err)}") from err async def handle_write_register(call: ServiceCall): """Generic write service (protocol-agnostic) with detailed logging.""" @@ -912,8 +900,8 @@ async def handle_write_register(call: ServiceCall): raise HomeAssistantError(f"Write failed for address {address}") except Exception as err: - _LOGGER.exception("Unexpected exception in write_register service for address %s", address) - raise HomeAssistantError(f"Write failed for address {address}: {err!s}") from err + _LOGGER.error("Unexpected exception in write_register service for address %s: %s", address, err, exc_info=True) + raise HomeAssistantError(f"Write failed for address {address}: {str(err)}") from err async def handle_read_register(call: ServiceCall): """Generic read service (protocol-agnostic).""" @@ -1066,7 +1054,7 @@ async def handle_read_bacnet(call: ServiceCall): address=address, entity_config=entity_config, ) - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.debug("BACnet Read failed with error: %s", err) if value is None: raise HomeAssistantError(f"Failed to read BACnet address {address}") @@ -1105,7 +1093,7 @@ async def handle_write_bacnet(call: ServiceCall): value=value, entity_config=entity_config, ) - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.debug("BACnet write failed with error: %s", err) if not success: @@ -1188,7 +1176,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not still_used: try: await client.disconnect() - except Exception as err: # noqa: BLE001 + except Exception as err: _LOGGER.debug("Error closing Modbus client: %s", err) else: # Other protocols (SNMP, MQTT, etc.) @@ -1205,7 +1193,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not still_used: try: await client.disconnect() - except Exception as err: # noqa: BLE001 + except Exception as err: _LOGGER.debug("Error closing client: %s", err) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/custom_components/protocol_wizard/config_flow.py b/custom_components/protocol_wizard/config_flow.py index 1522446..61c9a9e 100644 --- a/custom_components/protocol_wizard/config_flow.py +++ b/custom_components/protocol_wizard/config_flow.py @@ -1,64 +1,53 @@ """Config flow for Protocol Wizard.""" -import asyncio import logging from typing import Any - import serial.tools.list_ports import voluptuous as vol +import asyncio from homeassistant import config_entries +from homeassistant.helpers import selector +from homeassistant.data_entry_flow import FlowResult from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import selector -from pymodbus.client import ( - AsyncModbusSerialClient, - AsyncModbusTcpClient, - AsyncModbusUdpClient, -) -from pymodbus.exceptions import ModbusIOException - +from pymodbus.client import AsyncModbusSerialClient, AsyncModbusTcpClient, AsyncModbusUdpClient +from .protocols.mqtt import CONF_BROKER, DEFAULT_PORT, CONF_USERNAME, CONF_PASSWORD from .const import ( - CONF_BAUDRATE, - CONF_BYTESIZE, + CONNECTION_TYPE_SERIAL, + CONNECTION_TYPE_IP, + CONNECTION_TYPE_TCP, + CONNECTION_TYPE_UDP, CONF_CONNECTION_TYPE, - CONF_FIRST_REG, - CONF_FIRST_REG_SIZE, CONF_HOST, - CONF_IP, - CONF_NAME, - CONF_PARITY, CONF_PORT, - CONF_PROTOCOL, - CONF_PROTOCOL_BACNET, - CONF_PROTOCOL_MODBUS, - CONF_PROTOCOL_MQTT, - CONF_PROTOCOL_SNMP, CONF_SERIAL_PORT, CONF_SLAVE_ID, - CONF_SLAVES, + CONF_BAUDRATE, + CONF_PARITY, + CONF_NAME, CONF_STOPBITS, - CONF_TEMPLATE, + CONF_BYTESIZE, + CONF_FIRST_REG, + CONF_FIRST_REG_SIZE, CONF_UPDATE_INTERVAL, - CONNECTION_TYPE_IP, - CONNECTION_TYPE_SERIAL, - CONNECTION_TYPE_TCP, - CONNECTION_TYPE_UDP, + CONF_SLAVES, + DEFAULT_SLAVE_ID, DEFAULT_BAUDRATE, - DEFAULT_BYTESIZE, + DEFAULT_TCP_PORT, DEFAULT_PARITY, - DEFAULT_SLAVE_ID, DEFAULT_STOPBITS, - DEFAULT_TCP_PORT, + DEFAULT_BYTESIZE, DOMAIN, + CONF_PROTOCOL_MODBUS, + CONF_PROTOCOL_SNMP, + CONF_PROTOCOL_MQTT, + CONF_PROTOCOL_BACNET, + CONF_PROTOCOL, + CONF_IP, + CONF_TEMPLATE, ) from .options_flow import ProtocolWizardOptionsFlow from .protocols import ProtocolRegistry -from .protocols.mqtt import CONF_BROKER, CONF_PASSWORD, CONF_USERNAME, DEFAULT_PORT -from .template_utils import ( - get_available_templates, - get_template_dropdown_choices, - load_template, -) +from .template_utils import get_available_templates, get_template_dropdown_choices, load_template _LOGGER = logging.getLogger(__name__) # Reduce noise from pymodbus @@ -319,8 +308,8 @@ async def async_step_modbus_device(self, user_input: dict[str, Any] | None = Non }, ) - except (ModbusIOException, OSError, ConnectionError, TimeoutError): - _LOGGER.exception("Connection test failed") + except Exception as err: + _LOGGER.exception("Connection test failed: %s", err) errors["base"] = "cannot_connect" # Get available templates @@ -436,7 +425,7 @@ async def _async_test_modbus_connection(self, data: dict[str, Any]) -> None: if not result.isError() and hasattr(result, "registers") and len(result.registers) == count: success = True break - except (ModbusIOException, OSError, ConnectionError, TimeoutError) as inner_err: + except Exception as inner_err: _LOGGER.debug("Test read failed for %s at addr %d: %s", name, address, inner_err) if not success: @@ -450,7 +439,7 @@ async def _async_test_modbus_connection(self, data: dict[str, Any]) -> None: if client: try: client.close() - except Exception as err: # noqa: BLE001 + except Exception as err: _LOGGER.debug("Error closing Modbus client: %s", err) # ================================================================ @@ -494,8 +483,8 @@ async def async_step_snmp_common(self, user_input: dict[str, Any] | None = None) options=options, ) - except (OSError, ConnectionError, TimeoutError): - _LOGGER.exception("SNMP connection test failed") + except Exception as err: + _LOGGER.exception("SNMP connection test failed: %s", err) errors["base"] = "cannot_connect" # Get available templates @@ -637,7 +626,7 @@ async def async_step_bacnet_discover(self, user_input=None): except asyncio.TimeoutError: _LOGGER.warning("BACnet discovery timed out") errors["base"] = "discovery_timeout" - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("BACnet discovery failed: %s", err) errors["base"] = "discovery_failed" finally: @@ -645,7 +634,7 @@ async def async_step_bacnet_discover(self, user_input=None): try: await discovery_client.disconnect() _LOGGER.info("Discovery client disconnected") - except Exception as err: # noqa: BLE001 + except Exception as err: _LOGGER.warning("Error disconnecting discovery client: %s", err) # If no devices found or error, show option to go manual @@ -786,7 +775,7 @@ async def async_step_bacnet_template(self, user_input=None): ) else: errors["base"] = "cannot_connect" - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("BACnet connection test failed: %s", err) errors["base"] = "unknown" @@ -874,8 +863,8 @@ async def async_step_mqtt_common(self, user_input: dict[str, Any] | None = None) options=options, ) - except (OSError, ConnectionError, TimeoutError): - _LOGGER.exception("MQTT connection test failed") + except Exception as err: + _LOGGER.exception("MQTT connection test failed: %s", err) errors["base"] = "cannot_connect" # Get available templates @@ -937,21 +926,21 @@ async def _async_test_mqtt_connection(self, config: dict) -> None: connected = await client.connect() if not connected: - raise ConnectionError("Could not connect to MQTT broker") + raise Exception("Could not connect to MQTT broker") _LOGGER.info("MQTT connection test successful to %s:%s", config[CONF_BROKER], config[CONF_PORT]) - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("MQTT connection test failed: %s", err) - raise ConnectionError( + raise Exception( f"Cannot connect to MQTT broker at {config[CONF_BROKER]}:{config[CONF_PORT]}. " "Check broker address, port, and credentials." - ) from err + ) finally: if client: try: await client.disconnect() - except Exception as err: # noqa: BLE001 + except Exception as err: _LOGGER.debug("Error disconnecting MQTT client: %s", err) diff --git a/custom_components/protocol_wizard/entity_base.py b/custom_components/protocol_wizard/entity_base.py index 21590e0..0ad49e0 100644 --- a/custom_components/protocol_wizard/entity_base.py +++ b/custom_components/protocol_wizard/entity_base.py @@ -5,32 +5,30 @@ """Protocol-agnostic base entity classes.""" from __future__ import annotations -import json import logging -from abc import ABC, abstractmethod from typing import Any - -from homeassistant.components.number import NumberEntity, NumberMode -from homeassistant.components.select import SelectEntity -from homeassistant.components.sensor import SensorEntity -from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry +from abc import ABC, abstractmethod +import json from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.update_coordinator import CoordinatorEntity - +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.sensor import SensorEntity +from homeassistant.components.number import NumberEntity,NumberMode +from homeassistant.components.select import SelectEntity +from homeassistant.helpers import entity_registry as er +from homeassistant.components.switch import SwitchEntity from .const import ( - CONF_BACNET_DEVICES, CONF_ENTITIES, - CONF_PROTOCOL, - CONF_PROTOCOL_BACNET, + CONF_REGISTERS, CONF_PROTOCOL_MODBUS, - # CONF_PROTOCOL_SNMP, +# CONF_PROTOCOL_SNMP, CONF_PROTOCOL_MQTT, - CONF_REGISTERS, + CONF_PROTOCOL_BACNET, + CONF_PROTOCOL, CONF_SLAVES, + CONF_BACNET_DEVICES, SIGNAL_ENTITY_SYNC, ) from .protocols.base import BaseProtocolCoordinator @@ -181,14 +179,17 @@ def _handle_sync_signal() -> None: @abstractmethod def _should_create_entity(self, entity_config: dict) -> bool: """Determine if entity should be created for this config.""" + pass @abstractmethod def _create_entity(self, entity_config: dict, unique_id: str, key: str) -> Entity: """Create the appropriate entity type.""" + pass @abstractmethod def _get_entity_type_suffix(self) -> str: """Get suffix for unique_id (e.g., 'sensor', 'number').""" + pass def _get_entities_config_key(self) -> str: """Get the config key for entities list. Override if protocol uses different key.""" @@ -481,7 +482,7 @@ async def async_set_native_value(self, value: float) -> None: if register_type in ("coil", "discrete"): value = bool(int(float(value))) # "0" → False, "1" → True elif "float" not in self.data_type: - value = round(float(value)) # Regular registers + value = int(round(float(value))) # Regular registers else: value = float(value) # Float registers @@ -625,7 +626,7 @@ def __init__( if isinstance(options_raw, str): try: options_dict = json.loads(options_raw) - except (json.JSONDecodeError, ValueError): + except Exception: _LOGGER.error( "Invalid options JSON for %s: %r", entity_config.get("name"), @@ -710,7 +711,7 @@ async def async_select_option(self, option: str) -> None: elif "float" in self._config.get("data_type", ""): value = float(value) else: - value = round(float(value)) # Regular registers + value = int(round(float(value))) # Regular registers elif protocol == CONF_PROTOCOL_BACNET: # BACnet: convert based on data type @@ -744,7 +745,7 @@ async def async_select_option(self, option: str) -> None: else: _LOGGER.error("Failed to write value to %s", self._config.get("name")) - except (OSError, ConnectionError, TimeoutError, ValueError, TypeError) as err: + except Exception as err: _LOGGER.error("Error in async_select_option: %s", err) import traceback traceback.print_exc() @@ -798,7 +799,7 @@ def __init__( def native_value(self): try: return "connected" if self.coordinator.client.is_connected else "disconnected" - except (AttributeError, OSError, ConnectionError) as err: + except Exception as err: _LOGGER.debug("Failed to get hub status: %s", err) return "unknown" @@ -898,10 +899,9 @@ def get_all_coordinators_for_entry(hass: HomeAssistant, entry: ConfigEntry): Returns list of (coordinator, device_info) tuples. Handles multi-slave Modbus and multi-device BACnet. """ + from .const import DOMAIN, CONF_SLAVES, CONF_PROTOCOL, CONF_PROTOCOL_MODBUS from homeassistant.helpers.entity import DeviceInfo - from .const import CONF_PROTOCOL, CONF_PROTOCOL_MODBUS, CONF_SLAVES, DOMAIN - coordinator_keys = hass.data[DOMAIN].get("entry_coordinator_keys", {}).get( entry.entry_id, [entry.entry_id] ) diff --git a/custom_components/protocol_wizard/number.py b/custom_components/protocol_wizard/number.py index ad74eca..4d1313d 100644 --- a/custom_components/protocol_wizard/number.py +++ b/custom_components/protocol_wizard/number.py @@ -5,16 +5,11 @@ from __future__ import annotations import logging - -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry from .const import DOMAIN -from .entity_base import ( - BaseEntityManager, - ProtocolWizardNumberBase, - get_all_coordinators_for_entry, -) +from .entity_base import BaseEntityManager, ProtocolWizardNumberBase, get_all_coordinators_for_entry _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/protocol_wizard/options_flow.py b/custom_components/protocol_wizard/options_flow.py index e992170..067cfdb 100644 --- a/custom_components/protocol_wizard/options_flow.py +++ b/custom_components/protocol_wizard/options_flow.py @@ -4,38 +4,35 @@ """Options flow for Protocol Wizard – fully protocol-agnostic.""" from __future__ import annotations -import json import logging +import json from datetime import timedelta - import voluptuous as vol +from .template_utils import ( + save_template, + get_available_templates, + get_template_dropdown_choices, + load_template, + delete_template, +) from homeassistant import config_entries -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers import selector - +from homeassistant.helpers import selector, device_registry as dr #import asyncio from .const import ( - CONF_BACNET_DEVICES, - CONF_BYTE_ORDER, + DOMAIN, + CONF_UPDATE_INTERVAL, CONF_ENTITIES, + CONF_REGISTERS, CONF_PROTOCOL, - CONF_PROTOCOL_BACNET, CONF_PROTOCOL_MODBUS, - CONF_PROTOCOL_MQTT, CONF_PROTOCOL_SNMP, + CONF_PROTOCOL_MQTT, + CONF_PROTOCOL_BACNET, + CONF_BYTE_ORDER, + CONF_WORD_ORDER, CONF_REGISTER_TYPE, - CONF_REGISTERS, CONF_SLAVES, - CONF_UPDATE_INTERVAL, - CONF_WORD_ORDER, - DOMAIN, -) -from .template_utils import ( - delete_template, - get_available_templates, - get_template_dropdown_choices, - load_template, - save_template, + CONF_BACNET_DEVICES, ) _LOGGER = logging.getLogger(__name__) @@ -788,7 +785,7 @@ def process_input(user_input: dict, errors: dict, existing: dict | None = None) processed["options"] = json.loads(processed["options"]) if not isinstance(processed["options"], dict): processed["options"]="" # if it is rubish, we dont use it - except (json.JSONDecodeError, ValueError): + except Exception: errors["options"] = "" # Update with new values, handling empty strings properly for key, value in user_input.items(): diff --git a/custom_components/protocol_wizard/protocols/__init__.py b/custom_components/protocol_wizard/protocols/__init__.py index 21a3de0..b672040 100644 --- a/custom_components/protocol_wizard/protocols/__init__.py +++ b/custom_components/protocol_wizard/protocols/__init__.py @@ -2,17 +2,13 @@ #-- protocol init.py protocol wizard #------------------------------------------ """Protocol registry for Protocol Wizard.""" -from __future__ import annotations - -from typing import ClassVar - +from typing import Dict, Type from .base import BaseProtocolCoordinator - class ProtocolRegistry: """Registry of available protocols.""" - - _protocols: ClassVar[dict[str, type[BaseProtocolCoordinator]]] = {} + + _protocols: Dict[str, Type[BaseProtocolCoordinator]] = {} @classmethod def register(cls, protocol_name: str): @@ -23,7 +19,7 @@ def wrapper(coordinator_class): return wrapper @classmethod - def get_coordinator_class(cls, protocol_name: str) -> type[BaseProtocolCoordinator] | None: + def get_coordinator_class(cls, protocol_name: str) -> Type[BaseProtocolCoordinator] | None: """Get coordinator class for protocol.""" return cls._protocols.get(protocol_name) diff --git a/custom_components/protocol_wizard/protocols/bacnet/__init__.py b/custom_components/protocol_wizard/protocols/bacnet/__init__.py index 43c2440..d8eb3b6 100644 --- a/custom_components/protocol_wizard/protocols/bacnet/__init__.py +++ b/custom_components/protocol_wizard/protocols/bacnet/__init__.py @@ -2,28 +2,28 @@ #-- protocol BACnet init.py protocol wizard #------------------------------------------ """BACnet protocol plugin.""" +from .coordinator import BACnetCoordinator from .client import BACnetClient from .const import ( + CONF_ENTITIES, BACNET_DATA_TYPES, BACNET_OBJECT_TYPES, BACNET_PROPERTIES, BACNET_UNITS, - CONF_ENTITIES, entity_key, - format_bacnet_address, parse_bacnet_address, + format_bacnet_address, ) -from .coordinator import BACnetCoordinator __all__ = [ + "BACnetCoordinator", + "BACnetClient", + "CONF_ENTITIES", "BACNET_DATA_TYPES", "BACNET_OBJECT_TYPES", "BACNET_PROPERTIES", "BACNET_UNITS", - "CONF_ENTITIES", - "BACnetClient", - "BACnetCoordinator", "entity_key", - "format_bacnet_address", "parse_bacnet_address", + "format_bacnet_address", ] diff --git a/custom_components/protocol_wizard/protocols/bacnet/client.py b/custom_components/protocol_wizard/protocols/bacnet/client.py index d313a7e..9fef629 100644 --- a/custom_components/protocol_wizard/protocols/bacnet/client.py +++ b/custom_components/protocol_wizard/protocols/bacnet/client.py @@ -1,28 +1,24 @@ # protocols/bacnet/client.py """BACnet/IP client for Protocol Wizard using bacpypes3 - proper initialization.""" -import asyncio import logging -from typing import Any - -from homeassistant.components.network import async_get_adapters, async_get_source_ip +import asyncio +from typing import Any, Optional from homeassistant.core import HomeAssistant - +from homeassistant.components.network import async_get_source_ip, async_get_adapters #import sys _LOGGER = logging.getLogger(__name__) try: # from bacpypes3.settings import settings - # from bacpypes3.argparse import SimpleArgumentParser, create_log_handler - import ipaddress - from bacpypes3.app import Application +# from bacpypes3.local.device import DeviceObject + from bacpypes3.primitivedata import ObjectIdentifier from bacpypes3.basetypes import PropertyIdentifier from bacpypes3.pdu import Address, LocalBroadcast - - # from bacpypes3.local.device import DeviceObject - from bacpypes3.primitivedata import ObjectIdentifier +# from bacpypes3.argparse import SimpleArgumentParser, create_log_handler + import ipaddress HAS_BACPYPES3 = True except ImportError: HAS_BACPYPES3 = False @@ -64,7 +60,7 @@ async def get_my_lan_ip_and_subnet(hass): # 2. Fallback: first private LAN IP for entry in summary: ip = entry["ip"] - if ip.startswith(("192.168.", "10.", "172.")): + if ip.startswith("192.168.") or ip.startswith("10.") or ip.startswith("172."): return entry["ip"], entry["prefix"] # 3. Last resort: first IP @@ -85,7 +81,7 @@ def calculate_broadcast_address(ip_with_subnet): _LOGGER.debug("Network: %s, Broadcast: %s", network.network_address, broadcast) return ip, netmask, broadcast - except (ValueError, TypeError) as e: + except Exception as e: _LOGGER.error("Failed to calculate broadcast address from %s: %s", ip_with_subnet, e) return None, None, None @@ -107,9 +103,9 @@ def __init__( self, hass: HomeAssistant, host: str, - device_id: int | None = None, + device_id: Optional[int] = None, port: int = 47808, - network_number: int | None = 0 + network_number: Optional[int] = 0 ): """Initialize BACnet client.""" if not HAS_BACPYPES3: @@ -119,7 +115,7 @@ def __init__( self.device_id = device_id self.port = port self.network_number = network_number - self.app: Application | None = None + self.app: Optional[Application] = None self._connected = False self.hass = hass self._bacpypeinstance = None @@ -128,18 +124,18 @@ async def _initialize_bacpypes3(self, hass: HomeAssistant): """Initialize bacpypes3 properly using from_args pattern.""" if not self._bacpypeinstance: try: - import random from argparse import Namespace + import random source_ip = address_adapter = ip_to_use = self.host try: address_adapter = await get_my_lan_ip_and_subnet(hass) - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.warning("Error in getting adapter info: %s", err) try: source_ip = await async_get_source_ip(hass) - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.warning("Error in getting HA local IP info: %s", err) if self.host == "0.0.0.0": # Discovery mode - use actual IP @@ -198,7 +194,7 @@ async def _initialize_bacpypes3(self, hass: HomeAssistant): return theApp - except (OSError, ConnectionError, TimeoutError, RuntimeError) as err: + except Exception as err: _LOGGER.error("Failed to initialize bacpypes3: %s", err) import traceback traceback.print_exc() @@ -222,7 +218,7 @@ async def connect(self) -> bool: _LOGGER.debug("[BACnet] Connected successfully, app=%s", type(self.app).__name__) return True - except (OSError, ConnectionError, TimeoutError, RuntimeError) as err: + except Exception as err: _LOGGER.error("[BACnet] Connection failed: %s", err) import traceback traceback.print_exc() @@ -254,7 +250,7 @@ async def discover_devices(self, timeout: int = 10) -> list[dict]: await asyncio.sleep(0.5) - except (OSError, ConnectionError, TimeoutError, RuntimeError) as err: + except Exception as err: _LOGGER.error("Who-Is failed: %s", err) return [] @@ -266,7 +262,7 @@ async def discover_devices(self, timeout: int = 10) -> list[dict]: _LOGGER.debug("Discovered %d BACnet devices", len(devices)) return devices - except (OSError, ConnectionError, TimeoutError, RuntimeError) as err: + except Exception as err: _LOGGER.error("BACnet discovery failed: %s", err) return [] @@ -316,16 +312,16 @@ def _collect_discovered_devices(self) -> list[dict]: 'vendor': vendor, }) - except (ValueError, TypeError) as err: + except Exception as err: _LOGGER.warning("Error parsing device %s: %s", device_id, err) - except (ValueError, TypeError) as err: + except Exception as err: _LOGGER.error("Error collecting discovered devices: %s", err) return devices - async def get_device_name(self) -> str | None: + async def get_device_name(self) -> Optional[str]: """Get device name.""" try: if not self._connected or not self.device_id: @@ -333,7 +329,7 @@ async def get_device_name(self) -> str | None: name = await self.read_property("device", self.device_id, "objectName") return name - except (OSError, ConnectionError, TimeoutError, RuntimeError) as err: + except Exception as err: _LOGGER.warning("Could not read device name: %s", err) return None @@ -343,7 +339,7 @@ async def read_property( object_type: str, object_instance: int, property_name: str - ) -> Any | None: + ) -> Optional[Any]: """Read BACnet property.""" if not self._connected or not self.app: _LOGGER.error("Not connected to BACnet network") @@ -371,8 +367,8 @@ async def read_property( _LOGGER.warning("[BACnet] Read timed out after 5s - no response from %s for %s", device_address, object_id) return None - except (OSError, ConnectionError, TimeoutError, RuntimeError) as err: - _LOGGER.error("Read failed for %s:%s.%s: %s", + except Exception as err: + _LOGGER.error("Read failed for %s:%s.%s: %s", object_type, object_instance, property_name, err) import traceback traceback.print_exc() @@ -408,8 +404,8 @@ async def write_property( _LOGGER.debug("Wrote %s to %s:%s.%s", value, object_type, object_instance, property_name) return True - except (OSError, ConnectionError, TimeoutError, RuntimeError) as err: - _LOGGER.error("Write failed for %s:%s.%s: %s", + except Exception as err: + _LOGGER.error("Write failed for %s:%s.%s: %s", object_type, object_instance, property_name, err) import traceback traceback.print_exc() @@ -429,7 +425,7 @@ async def disconnect(self): await link_layer.close() else: link_layer.close() - except Exception as err: # noqa: BLE001 + except Exception as err: _LOGGER.debug("Error closing link layer %s: %s", port_id, err) # Close the application @@ -448,8 +444,8 @@ async def disconnect(self): _LOGGER.debug("BACnet disconnected") - except Exception as err: # noqa: BLE001 - _LOGGER.debug("Error disconnecting BACnet: %s", err) + except Exception as err: + _LOGGER.error("Error disconnecting BACnet: %s", err) finally: self.app = None self._bacpypeinstance = None diff --git a/custom_components/protocol_wizard/protocols/bacnet/coordinator.py b/custom_components/protocol_wizard/protocols/bacnet/coordinator.py index b516447..24ca7e6 100644 --- a/custom_components/protocol_wizard/protocols/bacnet/coordinator.py +++ b/custom_components/protocol_wizard/protocols/bacnet/coordinator.py @@ -1,20 +1,17 @@ # protocols/bacnet/coordinator.py """BACnet coordinator for Protocol Wizard.""" -import asyncio import logging -from datetime import timedelta from typing import Any - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - -from ...const import CONF_BACNET_DEVICES, CONF_ENTITIES, CONF_PROTOCOL_BACNET -from ...protocols.base import BaseProtocolCoordinator +import asyncio +from datetime import timedelta from .. import ProtocolRegistry +from ...protocols.base import BaseProtocolCoordinator +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from .const import parse_bacnet_address, entity_key from .client import BACnetClient -from .const import entity_key, parse_bacnet_address - +from ...const import CONF_ENTITIES, CONF_PROTOCOL_BACNET, CONF_BACNET_DEVICES _LOGGER = logging.getLogger(__name__) @ProtocolRegistry.register(CONF_PROTOCOL_BACNET) @@ -93,7 +90,7 @@ async def async_read_entity( ) return None - except (OSError, ConnectionError, TimeoutError, RuntimeError) as err: + except Exception as err: _LOGGER.error( "Error reading entity %s: %s", entity_config.get("name"), @@ -159,7 +156,7 @@ async def async_write_entity( _LOGGER.error("Invalid address for entity %s: %s", entity_config.get("name"), err) return False - except (OSError, ConnectionError, TimeoutError, RuntimeError) as err: + except Exception as err: _LOGGER.error("Write error for entity %s: %s", entity_config.get("name"), err) import traceback _LOGGER.error(traceback.format_exc()) @@ -337,7 +334,7 @@ async def _async_update_data(self) -> dict[str, Any]: failed_count += 1 consecutive_failures += 1 - except (OSError, ConnectionError, TimeoutError, RuntimeError) as err: + except Exception as err: _LOGGER.error( "Error reading entity %s: %s", entity.get("name"), @@ -375,7 +372,7 @@ async def _async_connect(self) -> bool: else: _LOGGER.warning("[BACnet] Connection failed") return False - except (OSError, ConnectionError, TimeoutError, RuntimeError) as err: + except Exception as err: _LOGGER.error("[BACnet] Connection error: %s", err) return False @@ -561,7 +558,7 @@ async def async_write_value( _LOGGER.error("Invalid address for entity %s: %s", entity_name, err) return False - except (OSError, ConnectionError, TimeoutError, RuntimeError) as err: + except Exception as err: _LOGGER.error("Write error for entity %s: %s", entity_name, err) return False diff --git a/custom_components/protocol_wizard/protocols/base.py b/custom_components/protocol_wizard/protocols/base.py index b34da7c..31f319a 100644 --- a/custom_components/protocol_wizard/protocols/base.py +++ b/custom_components/protocol_wizard/protocols/base.py @@ -8,12 +8,12 @@ import logging from abc import ABC, abstractmethod -from datetime import timedelta from typing import Any +from datetime import timedelta -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.config_entries import ConfigEntry _LOGGER = logging.getLogger(__name__) @@ -28,23 +28,28 @@ class BaseProtocolClient(ABC): @abstractmethod async def connect(self) -> bool: """Establish connection. Returns True if successful.""" + pass @abstractmethod async def disconnect(self) -> None: """Close connection.""" + pass @abstractmethod async def read(self, address: str, **kwargs) -> Any: """Read value from address. Protocol-specific kwargs.""" + pass @abstractmethod async def write(self, address: str, value: Any, **kwargs) -> bool: """Write value to address. Returns True if successful.""" + pass @property @abstractmethod def is_connected(self) -> bool: """Connection status.""" + pass class _SafeFormatDict(dict): """Returns placeholder unchanged if key is missing.""" @@ -82,6 +87,7 @@ async def _async_update_data(self) -> dict[str, Any]: Should return a dict where keys are entity identifiers and values are the decoded/processed values. """ + pass @abstractmethod def _decode_value(self, raw_value: Any, entity_config: dict) -> Any: @@ -95,6 +101,7 @@ def _decode_value(self, raw_value: Any, entity_config: dict) -> Any: Returns: Decoded Python value (int, float, str, bool, etc.) """ + pass @abstractmethod def _encode_value(self, value: Any, entity_config: dict) -> Any: @@ -108,6 +115,7 @@ def _encode_value(self, value: Any, entity_config: dict) -> Any: Returns: Protocol-specific encoded value """ + pass def _format_value(self, value: Any, entity_config: dict) -> Any: format_str = str(entity_config.get("format", "")).strip() @@ -126,7 +134,7 @@ def _format_value(self, value: Any, entity_config: dict) -> Any: try: numeric = float(value) except (TypeError, ValueError): - _LOGGER.debug("Value %r is not numeric, skipping numeric format helpers", value) + pass if numeric is not None: total = int(numeric) @@ -150,7 +158,7 @@ def _format_value(self, value: Any, entity_config: dict) -> Any: return result - except (ValueError, TypeError, KeyError) as err: + except Exception as err: _LOGGER.debug( "Format error for entity '%s': %s", entity_config.get("name"), @@ -177,6 +185,7 @@ async def async_read_entity( Returns: Decoded value or None if failed """ + pass @abstractmethod async def async_write_entity( @@ -198,6 +207,7 @@ async def async_write_entity( Returns: True if successful """ + pass async def _async_connect(self) -> bool: """ @@ -209,6 +219,6 @@ async def _async_connect(self) -> bool: try: return await self.client.connect() - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("[%s] Failed to connect: %s", self.protocol_name, err) return False diff --git a/custom_components/protocol_wizard/protocols/modbus/__init__.py b/custom_components/protocol_wizard/protocols/modbus/__init__.py index 29de62e..61d9c47 100644 --- a/custom_components/protocol_wizard/protocols/modbus/__init__.py +++ b/custom_components/protocol_wizard/protocols/modbus/__init__.py @@ -2,8 +2,8 @@ #-- protocol modbus init.py protocol wizard #------------------------------------------ """Modbus protocol plugin.""" +from .coordinator import ModbusCoordinator from .client import ModbusClient from .const import CONF_REGISTERS, TYPE_SIZES, reg_key -from .coordinator import ModbusCoordinator -__all__ = ["CONF_REGISTERS", "TYPE_SIZES", "ModbusClient", "ModbusCoordinator", "reg_key"] +__all__ = ["ModbusCoordinator", "ModbusClient", "CONF_REGISTERS", "TYPE_SIZES", "reg_key"] diff --git a/custom_components/protocol_wizard/protocols/modbus/client.py b/custom_components/protocol_wizard/protocols/modbus/client.py index b516cdc..1e67dc1 100644 --- a/custom_components/protocol_wizard/protocols/modbus/client.py +++ b/custom_components/protocol_wizard/protocols/modbus/client.py @@ -4,8 +4,8 @@ """Modbus protocol client wrapper.""" from __future__ import annotations -import asyncio import logging +import asyncio from typing import Any from pymodbus.exceptions import ModbusIOException @@ -59,7 +59,7 @@ async def connect(self) -> bool: await self._client.connect() self._conn_state["failed"] = False return self._client.connected - except (ModbusIOException, OSError, ConnectionError) as err: + except Exception as err: _LOGGER.error("Modbus connection failed: %s", err) self._conn_state["failed"] = True return False @@ -70,7 +70,7 @@ async def disconnect(self) -> None: if self._client.connected: self._client.close() self._conn_state["failed"] = False - except Exception as err: # noqa: BLE001 + except Exception as err: _LOGGER.debug("Error closing Modbus client: %s", err) async def reconnect(self) -> bool: @@ -99,8 +99,8 @@ async def reconnect(self) -> bool: # Force close regardless of state try: self._client.close() - except Exception: # noqa: BLE001 - _LOGGER.debug("Error closing Modbus client during reconnect") + except Exception: + pass # Small delay before reconnecting await asyncio.sleep(0.3) @@ -110,7 +110,7 @@ async def reconnect(self) -> bool: self._conn_state["failed"] = False _LOGGER.info("Modbus reconnection successful") return self._client.connected - except (ModbusIOException, OSError, ConnectionError) as err: + except Exception as err: _LOGGER.error("Modbus reconnection failed: %s", err) self._conn_state["failed"] = True return False @@ -163,7 +163,7 @@ async def read(self, address: str, **kwargs) -> Any: address, self.slave_id, err) self._conn_state["failed"] = True # Mark shared state for reconnection raise # Re-raise so coordinator can handle - except (OSError, ConnectionError) as err: + except Exception as err: _LOGGER.error("Modbus read error at %s (slave %d): %s", address, self.slave_id, err) self._conn_state["failed"] = True @@ -217,7 +217,7 @@ async def write(self, address: str, value: Any, **kwargs) -> bool: address, self.slave_id, err) self._conn_state["failed"] = True return False - except (OSError, ConnectionError) as err: + except Exception as err: _LOGGER.error("Modbus write failed at %s (slave %d): %s", address, self.slave_id, err) self._conn_state["failed"] = True diff --git a/custom_components/protocol_wizard/protocols/modbus/coordinator.py b/custom_components/protocol_wizard/protocols/modbus/coordinator.py index d40105f..43fefc6 100644 --- a/custom_components/protocol_wizard/protocols/modbus/coordinator.py +++ b/custom_components/protocol_wizard/protocols/modbus/coordinator.py @@ -4,21 +4,20 @@ """Modbus protocol coordinator implementation.""" from __future__ import annotations -import asyncio import logging -from datetime import timedelta +import asyncio from typing import Any +from datetime import timedelta -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry from pymodbus.client.mixin import ModbusClientMixin -from pymodbus.exceptions import ModbusIOException +from .client import ModbusClient -from ...const import CONF_REGISTERS, CONF_SLAVES -from .. import ProtocolRegistry from ..base import BaseProtocolCoordinator -from .client import ModbusClient +from .. import ProtocolRegistry from .const import TYPE_SIZES, reg_key +from ...const import CONF_REGISTERS,CONF_SLAVES _LOGGER = logging.getLogger(__name__) @@ -71,7 +70,7 @@ async def _async_connect_unlocked(self) -> bool: # Need to connect try: return await self.client.connect() - except (ModbusIOException, OSError, ConnectionError) as err: + except Exception as err: _LOGGER.error("[Modbus] Failed to connect: %s", err) return False @@ -219,8 +218,7 @@ async def _auto_detect_type(self, address: int, count: int) -> tuple[str, Any] | result = await method(address=address, count=count, device_id=self.client.slave_id) if not result.isError(): return name, result - except (ModbusIOException, OSError, ConnectionError): - _LOGGER.debug("Auto-detect: type %s failed at address %d", name, address) + except Exception: continue _LOGGER.warning("Auto-detect failed at address %d", address) @@ -242,7 +240,7 @@ async def _direct_read(self, reg_type: str, address: int, count: int) -> Any | N try: return await method(address=address, count=count, device_id=self.client.slave_id) - except (ModbusIOException, OSError, ConnectionError) as err: + except Exception as err: _LOGGER.error("Direct read failed for type %s: %s", reg_type, err) return None @@ -295,12 +293,13 @@ def _decode_value(self, raw_value: Any, entity_config: dict) -> Any | None: decoded = decoded * scale + offset # Preserve integer type for integer data types when result is whole number - if data_type in ("uint16", "int16", "uint32", "int32", "uint64", "int64") and isinstance(decoded, float) and decoded.is_integer(): - decoded = int(decoded) + if data_type in ("uint16", "int16", "uint32", "int32", "uint64", "int64"): + if isinstance(decoded, float) and decoded.is_integer(): + decoded = int(decoded) return decoded - except (ValueError, TypeError) as err: + except Exception as err: _LOGGER.error( "Error decoding register '%s' at address %s: %s", entity_config.get("name"), entity_config.get("address"), err @@ -349,22 +348,22 @@ def _encode_value(self, value: Any, entity_config: dict) -> list[int] | bool | N if scale != 0: try: value = (value - offset) / scale - except (ValueError, TypeError) as err: + except Exception as err: _LOGGER.error("Scale/offset failed for value %s: %s", original_value, err) return None # Single register integer if data_type in ("uint16", "int16"): try: - value = round(float(value)) - except (ValueError, TypeError): + value = int(round(float(value))) + except Exception: _LOGGER.error("Failed to convert to int for %s: %s", data_type, original_value) return None if data_type == "int16" and value < 0: value += 65536 value = max(0, min(65535, value)) return [value] - except (ValueError, TypeError) as err: + except Exception as err: _LOGGER.error("Encoding error %s (%s): %s", value, data_type, err) return None # Multi-register types @@ -380,7 +379,7 @@ def _encode_value(self, value: Any, entity_config: dict) -> list[int] | bool | N if target_type == ModbusClientMixin.DATATYPE.FLOAT32: value = float(value) else: - value = round(float(value)) + value = int(round(float(value))) try: return self.client.raw_client.convert_to_registers( @@ -388,7 +387,7 @@ def _encode_value(self, value: Any, entity_config: dict) -> list[int] | bool | N data_type=target_type, word_order=0 if word_order == "big" else 1, ) - except (ValueError, TypeError) as err: + except Exception as err: _LOGGER.error("pymodbus convert_to_registers failed for %s (%s): %s", original_value, data_type, err) return None # ---------------------------------------------------------------------------- @@ -396,6 +395,8 @@ def _encode_value(self, value: Any, entity_config: dict) -> list[int] | bool | N #------------------------------------------------------------------------------ async def async_read_entity(self, address: str, entity_config: dict, **kwargs) -> Any | None: + from pymodbus.exceptions import ModbusIOException + addr = int(address) size = kwargs.get("size") or TYPE_SIZES.get(entity_config.get("data_type", "uint16").lower(), 1) reg_type = kwargs.get("register_type") or entity_config.get("register_type", "holding") @@ -425,10 +426,10 @@ async def async_read_entity(self, address: str, entity_config: dict, **kwargs) - values = test_values detected_type = test_type break - except (ModbusIOException, OSError, ConnectionError): + except ModbusIOException: continue # Try next type - except (ModbusIOException, OSError, ConnectionError) as err: + except ModbusIOException as err: _LOGGER.warning("[Modbus] I/O error reading address %s: %s - will attempt reconnect on next request", address, err) # Connection will be recovered on next _async_connect call return None diff --git a/custom_components/protocol_wizard/protocols/mqtt/__init__.py b/custom_components/protocol_wizard/protocols/mqtt/__init__.py index a2f61f1..e54964c 100644 --- a/custom_components/protocol_wizard/protocols/mqtt/__init__.py +++ b/custom_components/protocol_wizard/protocols/mqtt/__init__.py @@ -1,26 +1,26 @@ # custom_components/protocol_wizard/protocols/mqtt/__init__.py """MQTT protocol implementation.""" -from ...const import CONF_ENTITIES, CONF_PORT from .client import MQTTClient +from .coordinator import MQTTCoordinator +from ...const import CONF_ENTITIES, CONF_PORT from .const import ( CONF_BROKER, - CONF_PASSWORD, CONF_USERNAME, - DATA_TYPES, + CONF_PASSWORD, DEFAULT_PORT, + DATA_TYPES, topic_key, ) -from .coordinator import MQTTCoordinator __all__ = [ - "CONF_BROKER", + "MQTTClient", + "MQTTCoordinator", "CONF_ENTITIES", - "CONF_PASSWORD", + "CONF_BROKER", "CONF_PORT", "CONF_USERNAME", - "DATA_TYPES", + "CONF_PASSWORD", "DEFAULT_PORT", - "MQTTClient", - "MQTTCoordinator", + "DATA_TYPES", "topic_key" ] diff --git a/custom_components/protocol_wizard/protocols/mqtt/client.py b/custom_components/protocol_wizard/protocols/mqtt/client.py index a93b533..a1164a0 100644 --- a/custom_components/protocol_wizard/protocols/mqtt/client.py +++ b/custom_components/protocol_wizard/protocols/mqtt/client.py @@ -3,8 +3,8 @@ from __future__ import annotations import asyncio -import json import logging +import json from typing import Any import paho.mqtt.client as mqtt_client @@ -64,7 +64,7 @@ def _on_connect(self, client, userdata, flags, rc): try: self._client.subscribe(topic, qos=0) # _LOGGER.debug("Resubscribed to %s", topic) - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("Failed to resubscribe to %s: %s", topic, err) else: _LOGGER.error("MQTT connection failed with code %s", rc) @@ -141,7 +141,7 @@ def do_connect(): _LOGGER.error("MQTT connection timeout to %s:%s", self.broker, self.port) return False - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("MQTT connection failed: %s", err) self._connected = False return False @@ -156,8 +156,8 @@ def do_disconnect(): await asyncio.get_event_loop().run_in_executor(None, do_disconnect) _LOGGER.info("MQTT disconnected and loop stopped") - except Exception as err: # noqa: BLE001 - _LOGGER.debug("Error during MQTT disconnect: %s", err) + except Exception as err: + _LOGGER.error("Error during MQTT disconnect: %s", err) self._connected = False @@ -197,7 +197,7 @@ def do_subscribe(): _LOGGER.error("Failed to subscribe to %s", topic) return False - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("MQTT subscribe error: %s", err) return False @@ -402,7 +402,7 @@ def do_publish(): return success - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("MQTT publish error: %s", err) return False diff --git a/custom_components/protocol_wizard/protocols/mqtt/coordinator.py b/custom_components/protocol_wizard/protocols/mqtt/coordinator.py index a0a0551..79ec977 100644 --- a/custom_components/protocol_wizard/protocols/mqtt/coordinator.py +++ b/custom_components/protocol_wizard/protocols/mqtt/coordinator.py @@ -2,19 +2,19 @@ """MQTT protocol coordinator implementation - Event-Driven Architecture.""" from __future__ import annotations -import asyncio -import json import logging -from datetime import timedelta from typing import Any +from datetime import timedelta +import asyncio +import json -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry -from ...const import CONF_ENTITIES, CONF_PROTOCOL_MQTT -from .. import ProtocolRegistry from ..base import BaseProtocolCoordinator +from .. import ProtocolRegistry from .client import MQTTClient +from ...const import CONF_ENTITIES, CONF_PROTOCOL_MQTT from .const import topic_key _LOGGER = logging.getLogger(__name__) @@ -93,7 +93,7 @@ async def _async_update_data(self) -> dict[str, Any]: # Store raw for debugging new_data[f"{key}_raw"] = payload - except (ValueError, TypeError, KeyError) as err: + except Exception as err: _LOGGER.warning( "Failed to read MQTT topic %s: %s", topic, @@ -203,7 +203,7 @@ def _decode_value(self, raw_value: Any, entity_config: dict) -> Any: return None return raw_value - except (json.JSONDecodeError, ValueError, TypeError) as err: + except Exception as err: _LOGGER.warning("Failed to decode value: %s", err) # If numeric entity, return None on error (shows as unavailable) # Otherwise return None to avoid invalid state @@ -333,7 +333,7 @@ async def async_read_entity( # Single topic - decode normally return self._decode_value(payload, entity_config) - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("Failed to read MQTT topic %s: %s", address, err) return None @@ -383,7 +383,7 @@ async def async_write_entity( retain=retain, ) - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("Failed to write to MQTT topic %s: %s", address, err) return False @@ -394,6 +394,6 @@ async def _async_connect(self) -> bool: try: return await self.client.connect() - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("Failed to connect to MQTT broker: %s", err) return False diff --git a/custom_components/protocol_wizard/protocols/snmp/__init__.py b/custom_components/protocol_wizard/protocols/snmp/__init__.py index 599502b..ce55fdd 100644 --- a/custom_components/protocol_wizard/protocols/snmp/__init__.py +++ b/custom_components/protocol_wizard/protocols/snmp/__init__.py @@ -3,8 +3,8 @@ #------------------------------------------ """SNMP protocol plugin.""" +from .coordinator import SNMPCoordinator from .client import SNMPClient from .const import CONF_ENTITIES, SNMP_DATA_TYPES, oid_key -from .coordinator import SNMPCoordinator -__all__ = ["CONF_ENTITIES", "SNMP_DATA_TYPES", "SNMPClient", "SNMPCoordinator", "oid_key"] +__all__ = ["SNMPCoordinator", "SNMPClient", "CONF_ENTITIES", "SNMP_DATA_TYPES", "oid_key"] diff --git a/custom_components/protocol_wizard/protocols/snmp/client.py b/custom_components/protocol_wizard/protocols/snmp/client.py index 3e5bfac..9e6c434 100644 --- a/custom_components/protocol_wizard/protocols/snmp/client.py +++ b/custom_components/protocol_wizard/protocols/snmp/client.py @@ -7,12 +7,12 @@ from typing import Any from pysnmp.hlapi.v3arch.asyncio import ( + SnmpEngine, CommunityData, + UdpTransportTarget, ContextData, - ObjectIdentity, ObjectType, - SnmpEngine, - UdpTransportTarget, + ObjectIdentity, get_cmd, set_cmd, walk_cmd, @@ -75,7 +75,7 @@ async def connect(self) -> bool: value = await self.read("1.3.6.1.2.1.1.1.0") # sysDescr self._connected = value is not None return self._connected - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("SNMP connection test failed for %s:%s: %s", self.host, self.port, err) self._connected = False return False @@ -85,7 +85,7 @@ async def disconnect(self) -> None: if self._engine: try: self._engine.close_dispatcher() - except Exception as err: # noqa: BLE001 + except Exception as err: _LOGGER.debug("Error closing SNMP dispatcher: %s", err) finally: self._engine = None @@ -119,7 +119,7 @@ async def read(self, address: str, **kwargs) -> Any | None: return var_binds[0][1] # Return just the value return None - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("SNMP read failed for OID %s: %s", address, err) return None @@ -177,7 +177,7 @@ async def walk(self, base_oid: str) -> list[Any]: pretty_value = value.prettyPrint() if hasattr(value, 'prettyPrint') else str(value) results.append((pretty_oid, pretty_value)) - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("SNMP walk failed for %s: %s", base_oid, err) return results @@ -208,7 +208,7 @@ async def write(self, address: str, value: Any, **kwargs) -> bool: return True - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("SNMP write failed for OID %s: %s", address, err) return False diff --git a/custom_components/protocol_wizard/protocols/snmp/coordinator.py b/custom_components/protocol_wizard/protocols/snmp/coordinator.py index 3464f46..b6a0ad4 100644 --- a/custom_components/protocol_wizard/protocols/snmp/coordinator.py +++ b/custom_components/protocol_wizard/protocols/snmp/coordinator.py @@ -2,16 +2,15 @@ """SNMP protocol coordinator implementation.""" from __future__ import annotations -import asyncio import logging -from datetime import timedelta from typing import Any - -from homeassistant.config_entries import ConfigEntry +from datetime import timedelta +import asyncio from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry -from .. import ProtocolRegistry from ..base import BaseProtocolCoordinator +from .. import ProtocolRegistry from .client import SNMPClient from .const import CONF_ENTITIES, oid_key @@ -114,7 +113,7 @@ async def _async_update_data(self) -> dict[str, Any]: formatted = self._format_value(decoded, entity) new_data[key] = formatted - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("Error processing %s %s: %s", read_mode, oid, err) failed_count += 1 consecutive_failures += 1 @@ -160,7 +159,7 @@ def _decode_value(self, raw_value: Any, entity_config: dict) -> Any | None: return decoded - except (ValueError, TypeError) as err: + except Exception as err: _LOGGER.error("Decode error for OID %s: %s", entity_config.get("address"), err) return None @@ -177,12 +176,12 @@ def _encode_value(self, value: Any, entity_config: dict) -> Any: value = (value - offset) / scale if data_type != "float": - value = round(float(value)) + value = int(round(float(value))) # pysnmp handles type mapping — just return clean Python value return value - except (ValueError, TypeError) as err: + except Exception as err: _LOGGER.error("Encode error for %s: %s", entity_config.get("name"), err) return None @@ -214,7 +213,7 @@ async def async_read_entity( return self._decode_value(raw_value, entity_config) - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("Service read failed for OID %s: %s", address, err) return None @@ -242,6 +241,6 @@ async def async_write_entity( return success - except (OSError, ConnectionError, TimeoutError) as err: + except Exception as err: _LOGGER.error("Write failed for OID %s: %s", address, err) return False diff --git a/custom_components/protocol_wizard/select.py b/custom_components/protocol_wizard/select.py index 64a6fc4..b4585bc 100644 --- a/custom_components/protocol_wizard/select.py +++ b/custom_components/protocol_wizard/select.py @@ -5,16 +5,11 @@ from __future__ import annotations import logging - -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry from .const import DOMAIN -from .entity_base import ( - BaseEntityManager, - ProtocolWizardSelectBase, - get_all_coordinators_for_entry, -) +from .entity_base import BaseEntityManager, ProtocolWizardSelectBase, get_all_coordinators_for_entry _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/protocol_wizard/sensor.py b/custom_components/protocol_wizard/sensor.py index bb539fe..f6f56df 100644 --- a/custom_components/protocol_wizard/sensor.py +++ b/custom_components/protocol_wizard/sensor.py @@ -5,17 +5,16 @@ from __future__ import annotations import logging - -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry -from .const import CONF_PROTOCOL, CONF_PROTOCOL_MODBUS, DOMAIN +from .const import DOMAIN, CONF_PROTOCOL, CONF_PROTOCOL_MODBUS from .entity_base import ( BaseEntityManager, - ModbusConnectionInfoEntity, - ModbusSlaveIdEntity, - ProtocolWizardHubEntity, ProtocolWizardSensorBase, + ProtocolWizardHubEntity, + ModbusSlaveIdEntity, + ModbusConnectionInfoEntity, get_all_coordinators_for_entry, ) diff --git a/custom_components/protocol_wizard/switch.py b/custom_components/protocol_wizard/switch.py index 450dc5a..e707f00 100644 --- a/custom_components/protocol_wizard/switch.py +++ b/custom_components/protocol_wizard/switch.py @@ -3,16 +3,11 @@ from __future__ import annotations import logging - -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry from .const import DOMAIN -from .entity_base import ( - BaseEntityManager, - ProtocolWizardSwitchBase, - get_all_coordinators_for_entry, -) +from .entity_base import BaseEntityManager, ProtocolWizardSwitchBase, get_all_coordinators_for_entry _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/protocol_wizard/template_utils.py b/custom_components/protocol_wizard/template_utils.py index 80bac7a..cd72800 100644 --- a/custom_components/protocol_wizard/template_utils.py +++ b/custom_components/protocol_wizard/template_utils.py @@ -6,22 +6,15 @@ """ from __future__ import annotations -import json import logging - +import json #import os from pathlib import Path from typing import Any from homeassistant.core import HomeAssistant -from .const import ( - CONF_PROTOCOL_BACNET, - CONF_PROTOCOL_MODBUS, - CONF_PROTOCOL_MQTT, - CONF_PROTOCOL_SNMP, - DOMAIN, -) +from .const import DOMAIN, CONF_PROTOCOL_MODBUS, CONF_PROTOCOL_SNMP , CONF_PROTOCOL_MQTT, CONF_PROTOCOL_BACNET _LOGGER = logging.getLogger(__name__) @@ -68,7 +61,7 @@ def ensure_user_template_dirs(hass: HomeAssistant) -> None: try: base_path = Path(hass.config.path(USER_TEMPLATES_DIR)) - for protocol in PROTOCOL_SUBDIRS: + for protocol in PROTOCOL_SUBDIRS.keys(): protocol_dir = base_path / protocol protocol_dir.mkdir(parents=True, exist_ok=True) @@ -77,7 +70,7 @@ def ensure_user_template_dirs(hass: HomeAssistant) -> None: if not readme_path.exists(): readme_path.write_text(_get_readme_content(), encoding='utf-8') _LOGGER.info("Created user templates directory: %s", base_path) - except OSError as err: + except Exception as err: _LOGGER.warning("Failed to create user template directories: %s", err) @@ -142,7 +135,7 @@ def list_builtin(): "source": "builtin", "path": str(builtin_dir / f"{name}.json"), } - except OSError as err: + except Exception as err: _LOGGER.warning("Failed to list built-in templates for %s: %s", protocol, err) # Load user templates @@ -164,7 +157,7 @@ def list_user(): "source": "user", "path": str(user_dir / f"{name}.json"), } - except OSError as err: + except Exception as err: _LOGGER.warning("Failed to list user templates for %s: %s", protocol, err) return templates @@ -219,7 +212,7 @@ def read_template(): except json.JSONDecodeError as err: _LOGGER.error("Failed to parse template %s: %s", template_id, err) return None - except OSError as err: + except Exception as err: _LOGGER.error("Failed to load template %s: %s", template_id, err) return None @@ -296,7 +289,7 @@ def write_file(): relative_path = template_path.relative_to(Path(hass.config.config_dir)) _LOGGER.info("Saved template to %s", template_path) return True, f"Template saved to {relative_path}" - except OSError as err: + except Exception as err: _LOGGER.error("Failed to save template: %s", err) return False, f"Failed to save: {err}" @@ -332,7 +325,7 @@ async def delete_template( await hass.async_add_executor_job(template_path.unlink) _LOGGER.info("Deleted template: %s", template_path) return True, "Template deleted" - except OSError as err: + except Exception as err: _LOGGER.error("Failed to delete template: %s", err) return False, f"Failed to delete: {err}" @@ -376,7 +369,7 @@ async def get_available_templates_legacy( Returns list of filenames without .json extension, from both directories. """ templates = await get_available_templates(hass, protocol) - return [tid.split(":", 1)[1] for tid in templates] + return [tid.split(":", 1)[1] for tid in templates.keys()] async def load_template_legacy( diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..fb81e54 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,10 @@ +# Pin the lint rule set so results are deterministic across ruff versions. +# +# This is ruff's documented default selection: pycodestyle errors (E4/E7/E9) +# and pyflakes (F). Stylistic/opinionated rule families (BLE, TRY, S, G, SIM, +# UP, RUF, I) are deliberately not enabled: BLE001 in particular would push us +# to narrow `except Exception` in the protocol clients, and broad catches there +# are intentional — a misbehaving device must never propagate an exception into +# Home Assistant's event loop. +[lint] +select = ["E4", "E7", "E9", "F"]