Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions custom_components/pytap/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/pytap/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 34 additions & 35 deletions custom_components/pytap/pytap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand Down
7 changes: 3 additions & 4 deletions custom_components/pytap/pytap/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 2 additions & 4 deletions custom_components/pytap/pytap/core/barcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 3 additions & 4 deletions custom_components/pytap/pytap/core/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down
29 changes: 14 additions & 15 deletions custom_components/pytap/pytap/core/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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
Expand All @@ -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 = (
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions custom_components/pytap/pytap/core/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""

import socket
from typing import Optional


class TcpSource:
Expand All @@ -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."""
Expand Down Expand Up @@ -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):
Expand Down
10 changes: 4 additions & 6 deletions custom_components/pytap/pytap/core/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions custom_components/pytap/pytap/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion custom_components/pytap/pytap/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down
2 changes: 1 addition & 1 deletion custom_components/pytap/pytap/tests/test_barcode.py
Original file line number Diff line number Diff line change
@@ -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])
Expand Down
3 changes: 1 addition & 2 deletions custom_components/pytap/pytap/tests/test_parser.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Loading