From cb79f73dd66b831444ea9728877402c84d94898c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:05:52 +0000 Subject: [PATCH 1/2] Initial plan From 68974b2bfc385fe864bf20963707b519bd159ea9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:08:31 +0000 Subject: [PATCH 2/2] feat: add ruff.toml configuration and fix all lint issues Co-authored-by: azebro <1090464+azebro@users.noreply.github.com> --- custom_components/pytap/config_flow.py | 5 +- custom_components/pytap/coordinator.py | 2 +- custom_components/pytap/pytap/__init__.py | 69 +++++++++---------- custom_components/pytap/pytap/api.py | 7 +- custom_components/pytap/pytap/core/barcode.py | 6 +- custom_components/pytap/pytap/core/events.py | 7 +- custom_components/pytap/pytap/core/parser.py | 29 ++++---- custom_components/pytap/pytap/core/source.py | 5 +- custom_components/pytap/pytap/core/state.py | 10 ++- custom_components/pytap/pytap/core/types.py | 6 +- .../pytap/pytap/tests/test_api.py | 3 +- .../pytap/pytap/tests/test_barcode.py | 2 +- .../pytap/pytap/tests/test_parser.py | 3 +- .../pytap/pytap/tests/test_types.py | 11 +-- custom_components/pytap/sensor.py | 2 +- ruff.toml | 19 +++++ 16 files changed, 98 insertions(+), 88 deletions(-) create mode 100644 ruff.toml diff --git a/custom_components/pytap/config_flow.py b/custom_components/pytap/config_flow.py index 22693d9..9d648f5 100644 --- a/custom_components/pytap/config_flow.py +++ b/custom_components/pytap/config_flow.py @@ -12,10 +12,8 @@ import logging import re -import uuid from typing import Any - -import voluptuous as vol +import uuid from homeassistant.config_entries import ( ConfigEntry, @@ -26,6 +24,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +import voluptuous as vol from .const import ( CONF_MODULE_BARCODE, diff --git a/custom_components/pytap/coordinator.py b/custom_components/pytap/coordinator.py index 1ae80a9..a0e0317 100644 --- a/custom_components/pytap/coordinator.py +++ b/custom_components/pytap/coordinator.py @@ -8,10 +8,10 @@ from __future__ import annotations import asyncio +from datetime import datetime import logging import threading import time -from datetime import datetime from typing import Any from homeassistant.config_entries import ConfigEntry diff --git a/custom_components/pytap/pytap/__init__.py b/custom_components/pytap/pytap/__init__.py index 72abf28..efd5268 100644 --- a/custom_components/pytap/pytap/__init__.py +++ b/custom_components/pytap/pytap/__init__.py @@ -6,55 +6,54 @@ __version__ = "0.1.0" # Core types -from .core.types import ( - GatewayID, - Address, - FrameType, - Frame, - NodeID, - NodeAddress, - LongAddress, - RSSI, - SlotCounter, - PacketType, - ReceivedPacketHeader, - U12Pair, - PowerReport, - GatewayInfo, - NodeInfo, - iter_received_packets, +# API functions +from .api import ( + connect, + create_parser, + parse_bytes, ) +# Barcode utilities +from .core.barcode import barcode_from_address, decode_barcode, encode_barcode + +# CRC +from .core.crc import crc + # Events from .core.events import ( Event, - PowerReportEvent, InfrastructureEvent, - TopologyEvent, + PowerReportEvent, StringEvent, + TopologyEvent, ) +# Parser +from .core.parser import Parser + # State management from .core.state import ( - SlotClock, NodeTableBuilder, PersistentState, + SlotClock, ) - -# Parser -from .core.parser import Parser - -# Barcode utilities -from .core.barcode import encode_barcode, decode_barcode, barcode_from_address - -# CRC -from .core.crc import crc - -# API functions -from .api import ( - create_parser, - parse_bytes, - connect, +from .core.types import ( + RSSI, + Address, + Frame, + FrameType, + GatewayID, + GatewayInfo, + LongAddress, + NodeAddress, + NodeID, + NodeInfo, + PacketType, + PowerReport, + ReceivedPacketHeader, + SlotCounter, + U12Pair, + iter_received_packets, ) __all__ = [ diff --git a/custom_components/pytap/pytap/api.py b/custom_components/pytap/pytap/api.py index b045972..24dc213 100644 --- a/custom_components/pytap/pytap/api.py +++ b/custom_components/pytap/pytap/api.py @@ -3,18 +3,17 @@ from __future__ import annotations import logging -from typing import Optional -from .core.parser import Parser from .core.events import Event +from .core.parser import Parser +from .core.source import SerialSource, TcpSource from .core.state import PersistentState -from .core.source import TcpSource, SerialSource logger = logging.getLogger(__name__) def create_parser( - persistent_state: Optional[PersistentState] = None, + persistent_state: PersistentState | None = None, ) -> Parser: """Create a new protocol parser instance. diff --git a/custom_components/pytap/pytap/core/barcode.py b/custom_components/pytap/pytap/core/barcode.py index 48d288a..0bf04dc 100644 --- a/custom_components/pytap/pytap/core/barcode.py +++ b/custom_components/pytap/pytap/core/barcode.py @@ -8,8 +8,6 @@ Only addresses with prefix 04:C0:5B are barcode-eligible. """ -from typing import Optional - # 16-character barcode alphabet (no vowels) _ALPHABET = 'GHJKLMNPRSTVWXYZ' @@ -43,7 +41,7 @@ def _compute_barcode_crc(address_bytes: bytes) -> int: return crc -def encode_barcode(address_bytes: bytes) -> Optional[str]: +def encode_barcode(address_bytes: bytes) -> str | None: """Encode an 8-byte MAC address to a Tigo barcode string. Returns None if the address doesn't have the 04:C0:5B prefix. @@ -116,7 +114,7 @@ def decode_barcode(barcode: str) -> bytes: return address -def barcode_from_address(address_bytes: bytes) -> Optional[str]: +def barcode_from_address(address_bytes: bytes) -> str | None: """Convenience: encode barcode from address bytes, returning None if not eligible.""" try: return encode_barcode(address_bytes) diff --git a/custom_components/pytap/pytap/core/events.py b/custom_components/pytap/pytap/core/events.py index ffcffb0..527b4d2 100644 --- a/custom_components/pytap/pytap/core/events.py +++ b/custom_components/pytap/pytap/core/events.py @@ -3,9 +3,8 @@ All events are dataclass instances with a to_dict() method for JSON serialization. """ -from dataclasses import dataclass, asdict +from dataclasses import asdict, dataclass from datetime import datetime -from typing import Optional @dataclass @@ -28,7 +27,7 @@ class PowerReportEvent(Event): gateway_id: int node_id: int - barcode: Optional[str] + barcode: str | None voltage_in: float voltage_out: float current_in: float @@ -43,7 +42,7 @@ def __init__( *, gateway_id: int, node_id: int, - barcode: Optional[str], + barcode: str | None, voltage_in: float, voltage_out: float, current_in: float, diff --git a/custom_components/pytap/pytap/core/parser.py b/custom_components/pytap/pytap/core/parser.py index ee6be1e..a5f2d55 100644 --- a/custom_components/pytap/pytap/core/parser.py +++ b/custom_components/pytap/pytap/core/parser.py @@ -5,33 +5,32 @@ feed(bytes) -> list[Event] interface. """ -import logging -import struct -from dataclasses import dataclass, asdict +from dataclasses import asdict, dataclass from datetime import datetime from enum import Enum, auto -from typing import Optional +import logging +import struct from .barcode import barcode_from_address from .crc import crc from .events import ( Event, - PowerReportEvent, InfrastructureEvent, - TopologyEvent, + PowerReportEvent, StringEvent, + TopologyEvent, ) -from .state import SlotClock, NodeTableBuilder, PersistentState +from .state import NodeTableBuilder, PersistentState, SlotClock from .types import ( Address, - FrameType, Frame, - NodeAddress, + FrameType, LongAddress, - SlotCounter, + NodeAddress, PacketType, - ReceivedPacketHeader, PowerReport, + ReceivedPacketHeader, + SlotCounter, iter_received_packets, ) @@ -131,7 +130,7 @@ class Parser: def __init__( self, - persistent_state: Optional[PersistentState] = None, + persistent_state: PersistentState | None = None, ): # Frame accumulator state self._state: _FrameState = _FrameState.IDLE @@ -147,7 +146,7 @@ def __init__( self._captured_slot_times: dict[int, datetime] = {} # Enumeration state - self._enum_state: Optional[_EnumerationState] = None + self._enum_state: _EnumerationState | None = None # Infrastructure — caller owns persistence; parser only mutates in memory self._persistent_state: PersistentState = ( @@ -208,7 +207,7 @@ def counters(self) -> dict: # Frame Accumulation State Machine # ------------------------------------------------------------------- - def _accumulate(self, byte: int) -> Optional[Frame]: + def _accumulate(self, byte: int) -> Frame | None: """Process a single byte. Returns a Frame when a complete valid frame is found.""" old_state = self._state @@ -299,7 +298,7 @@ def _accumulate(self, byte: int) -> Optional[Frame]: self._state = next_state return None - def _decode_frame(self, buffer: bytearray) -> Optional[Frame]: + def _decode_frame(self, buffer: bytearray) -> Frame | None: """Decode a completed frame from the buffer.""" if len(buffer) < 6: self._counters.runts += 1 diff --git a/custom_components/pytap/pytap/core/source.py b/custom_components/pytap/pytap/core/source.py index 2ac0068..16336b6 100644 --- a/custom_components/pytap/pytap/core/source.py +++ b/custom_components/pytap/pytap/core/source.py @@ -4,7 +4,6 @@ """ import socket -from typing import Optional class TcpSource: @@ -13,7 +12,7 @@ class TcpSource: def __init__(self, host: str, port: int = 502): self._host = host self._port = port - self._socket: Optional[socket.socket] = None + self._socket: socket.socket | None = None def connect(self): """Open a TCP connection to the host.""" @@ -43,7 +42,7 @@ def read(self, size: int = 1024) -> bytes: # Peer closed connection raise ConnectionResetError("Connection closed by peer") return data - except socket.timeout: + except TimeoutError: return b"" def close(self): diff --git a/custom_components/pytap/pytap/core/state.py b/custom_components/pytap/pytap/core/state.py index fb11a64..c0a8d4b 100644 --- a/custom_components/pytap/pytap/core/state.py +++ b/custom_components/pytap/pytap/core/state.py @@ -6,16 +6,14 @@ from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Optional from .types import ( - SlotCounter, + SLOTS_PER_EPOCH, LongAddress, NodeAddress, - SLOTS_PER_EPOCH, + SlotCounter, ) - # --------------------------------------------------------------------------- # SlotClock # --------------------------------------------------------------------------- @@ -33,7 +31,7 @@ class SlotClock: NUM_INDICES = 48 # 4 epochs x 12 indices each def __init__(self, slot_counter: SlotCounter, time: datetime): - self._times: list[Optional[datetime]] = [None] * self.NUM_INDICES + self._times: list[datetime | None] = [None] * self.NUM_INDICES self._last_index: int = -1 self._last_time: datetime = time self._initialize(slot_counter, time) @@ -104,7 +102,7 @@ def push( self, start_address: NodeAddress, entries: list[tuple[NodeAddress, LongAddress]], - ) -> Optional[dict[int, LongAddress]]: + ) -> dict[int, LongAddress] | None: """Add a page. Returns the complete table when an empty page arrives.""" if len(entries) == 0: result = dict(self._entries) diff --git a/custom_components/pytap/pytap/core/types.py b/custom_components/pytap/pytap/core/types.py index 9e6c098..77bd25f 100644 --- a/custom_components/pytap/pytap/core/types.py +++ b/custom_components/pytap/pytap/core/types.py @@ -7,11 +7,11 @@ - Infrastructure: GatewayInfo, NodeInfo """ -import struct +from collections.abc import Iterator from dataclasses import dataclass from enum import IntEnum -from typing import ClassVar, Iterator - +import struct +from typing import ClassVar # --------------------------------------------------------------------------- # Constants diff --git a/custom_components/pytap/pytap/tests/test_api.py b/custom_components/pytap/pytap/tests/test_api.py index 7748687..24b2142 100644 --- a/custom_components/pytap/pytap/tests/test_api.py +++ b/custom_components/pytap/pytap/tests/test_api.py @@ -1,8 +1,9 @@ """Tests for the public API module.""" import pytest -from pytap.core.parser import Parser + import pytap +from pytap.core.parser import Parser def test_create_parser(): diff --git a/custom_components/pytap/pytap/tests/test_barcode.py b/custom_components/pytap/pytap/tests/test_barcode.py index ffd5b27..35f4847 100644 --- a/custom_components/pytap/pytap/tests/test_barcode.py +++ b/custom_components/pytap/pytap/tests/test_barcode.py @@ -1,8 +1,8 @@ """Tests for the barcode encoding/decoding module.""" import pytest -from pytap.core.barcode import encode_barcode, decode_barcode, barcode_from_address +from pytap.core.barcode import barcode_from_address, decode_barcode, encode_barcode # Known address from ENUMERATION_SEQUENCE KNOWN_ADDRESS = bytes([0x04, 0xC0, 0x5B, 0x30, 0x00, 0x02, 0xBE, 0x16]) diff --git a/custom_components/pytap/pytap/tests/test_parser.py b/custom_components/pytap/pytap/tests/test_parser.py index 1b0c8db..c865d94 100644 --- a/custom_components/pytap/pytap/tests/test_parser.py +++ b/custom_components/pytap/pytap/tests/test_parser.py @@ -1,8 +1,7 @@ """Tests for the core protocol parser.""" -from pytap.core.parser import Parser from pytap.core.crc import crc - +from pytap.core.parser import Parser # ----------------------------------------------------------------------- # ENUMERATION_SEQUENCE test data (from Appendix B of implementation plan) diff --git a/custom_components/pytap/pytap/tests/test_types.py b/custom_components/pytap/pytap/tests/test_types.py index 8cf02a6..fc1f969 100644 --- a/custom_components/pytap/pytap/tests/test_types.py +++ b/custom_components/pytap/pytap/tests/test_types.py @@ -1,21 +1,22 @@ """Tests for protocol types.""" import struct + import pytest + from pytap.core.types import ( - GatewayID, Address, FrameType, - NodeID, + GatewayID, LongAddress, - SlotCounter, + NodeID, + PowerReport, ReceivedPacketHeader, + SlotCounter, U12Pair, - PowerReport, iter_received_packets, ) - # ---- GatewayID ---- diff --git a/custom_components/pytap/sensor.py b/custom_components/pytap/sensor.py index 0d33987..6f9b857 100644 --- a/custom_components/pytap/sensor.py +++ b/custom_components/pytap/sensor.py @@ -21,8 +21,8 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - EntityCategory, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + EntityCategory, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..8a034cc --- /dev/null +++ b/ruff.toml @@ -0,0 +1,19 @@ +line-length = 110 + +[lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade +] +ignore = [] + +[format] +quote-style = "double" +indent-style = "space" + +[lint.isort] +force-sort-within-sections = true +known-first-party = ["custom_components.pytap"]