From b9c28ff402ab35feab997697a1d3a1fe0e00bd3f Mon Sep 17 00:00:00 2001 From: Adam Zebrowski Date: Mon, 23 Feb 2026 17:55:45 +0000 Subject: [PATCH 01/10] feat: HACS compatibility and v1.0.0 release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add hacs.json manifest for HACS custom repository support - Add GitHub Actions: hassfest, HACS validation, ruff lint, pytest - Add release workflow to package pytap.zip on GitHub release - Update README with HACS install instructions and CI badges - Bump version 0.3.0 → 1.0.0 - Mark HACS Distribution as implemented in future_considerations --- .github/workflows/release.yml | 31 +++++++++++++++++ .github/workflows/validate.yml | 49 +++++++++++++++++++++++++++ README.md | 10 ++++-- custom_components/pytap/manifest.json | 2 +- hacs.json | 4 +++ planning/future_considerations.md | 8 +++-- 6 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/validate.yml create mode 100644 hacs.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f3d189e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: Release + +on: + release: + types: + - published + +permissions: + contents: write + +jobs: + release: + name: Prepare release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create release archive + run: | + cd custom_components/pytap + zip -r ../../pytap.zip . -x '__pycache__/*' '*/__pycache__/*' '.pytest_cache/*' + + - name: Upload release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./pytap.zip + asset_name: pytap.zip + asset_content_type: application/zip diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..6ebde56 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,49 @@ +name: Validate + +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + +jobs: + hassfest: + name: Hassfest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: home-assistant/actions/hassfest@master + + hacs: + name: HACS Validation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: hacs/action@main + with: + category: integration + + lint: + name: Ruff Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install ruff + - run: ruff check custom_components/pytap/ + + tests: + name: Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install -r requirements.txt + - run: python -m pytest tests/ -vv --tb=short diff --git a/README.md b/README.md index 3fc3630..72b8bb3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![HA Version](https://img.shields.io/badge/Home%20Assistant-2024.1%2B-blue.svg)](https://www.home-assistant.io/) +[![HACS Custom](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://hacs.xyz/) +[![Validate](https://github.com/azebro/pytap/actions/workflows/validate.yml/badge.svg)](https://github.com/azebro/pytap/actions/workflows/validate.yml) A Home Assistant custom integration for monitoring **Tigo TAP solar energy systems**. PyTap connects to your Tigo gateway over TCP, passively listens to the RS-485 bus protocol, and exposes real-time per-optimizer sensor entities — power, voltage, current, temperature, and more. @@ -62,9 +64,13 @@ PyTap also creates aggregate virtual devices: 1. Copy the `custom_components/pytap` folder into your Home Assistant `config/custom_components/` directory. 2. Restart Home Assistant. -### HACS (coming soon) +### HACS (Recommended) -HACS distribution is planned for a future release. +1. Open **HACS** in your Home Assistant instance. +2. Click the **three dots** menu in the top right and select **Custom repositories**. +3. Add `https://github.com/azebro/pytap` with category **Integration**. +4. Search for **PyTap** and click **Download**. +5. Restart Home Assistant. --- diff --git a/custom_components/pytap/manifest.json b/custom_components/pytap/manifest.json index 3ad22a8..67797d5 100644 --- a/custom_components/pytap/manifest.json +++ b/custom_components/pytap/manifest.json @@ -10,5 +10,5 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/azebro/pytap/issues", "requirements": [], - "version": "0.3.0" + "version": "1.0.0" } \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..65800ed --- /dev/null +++ b/hacs.json @@ -0,0 +1,4 @@ +{ + "name": "PyTap", + "render_readme": true +} \ No newline at end of file diff --git a/planning/future_considerations.md b/planning/future_considerations.md index adc1d17..9205114 100644 --- a/planning/future_considerations.md +++ b/planning/future_considerations.md @@ -46,13 +46,17 @@ Status: ✅ Implemented in Feature 5. 5.2 **Per-sensor readings counter** — Each optimizer now has a `readings_today` daily meter (`SensorStateClass.TOTAL`, diagnostic category) to support per-module connectivity troubleshooting. -## 6. Binary Sensor Platform +## 6. Binary Sensor Platform - low priority Add binary sensors for node connectivity (available/unavailable based on `last_update` age) and gateway online status. ## 7. HACS Distribution -Package for distribution via [HACS](https://hacs.xyz/) with a `hacs.json` manifest for one-click installation. +Status: ✅ Implemented in v1.0.0. + +- `hacs.json` manifest added for HACS compatibility. +- GitHub Actions workflows: hassfest validation, HACS validation, ruff lint, pytest, and release asset packaging. +- README updated with HACS installation instructions and badges. ## 8. Configuration - Add ability to bulk load devices From 5d9bb5d46bee3cc18f9d1751335b016bd6285c34 Mon Sep 17 00:00:00 2001 From: Adam Zebrowski Date: Mon, 23 Feb 2026 18:00:49 +0000 Subject: [PATCH 02/10] fix: CI validation errors - Add requirements-test.txt with pytest dependencies for CI - Update validate workflow to install test requirements - Ignore HACS brands check (requires external PR to home-assistant/brands) - Repository topics added via GitHub API for HACS topics check --- .github/workflows/validate.yml | 3 ++- requirements-test.txt | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 requirements-test.txt diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6ebde56..d6543b5 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -25,6 +25,7 @@ jobs: - uses: hacs/action@main with: category: integration + ignore: brands lint: name: Ruff Lint @@ -45,5 +46,5 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" - - run: pip install -r requirements.txt + - run: pip install -r requirements.txt -r requirements-test.txt - run: python -m pytest tests/ -vv --tb=short diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..e6d8224 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,7 @@ +# Test dependencies +pytest==9.0.0 +pytest-asyncio==1.3.0 +pytest-homeassistant-custom-component==0.13.315 +pytest_freezer==0.4.9 +pytest-socket==0.7.0 +pytest-timeout==2.4.0 From 18291569a2a743219a663cafa188eba792616e8a Mon Sep 17 00:00:00 2001 From: Adam Zebrowski Date: Mon, 23 Feb 2026 18:03:30 +0000 Subject: [PATCH 03/10] fix: tests CI - remove conflicting HA core pins pytest-homeassistant-custom-component transitively installs a compatible HA core; the explicit requirements.txt pins caused version conflicts. --- .github/workflows/validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index d6543b5..b859b9c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -46,5 +46,5 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" - - run: pip install -r requirements.txt -r requirements-test.txt + - run: pip install -r requirements-test.txt - run: python -m pytest tests/ -vv --tb=short From 7beb3aaf3e5b428783e7b65876d667a8124a2e64 Mon Sep 17 00:00:00 2001 From: Adam Zebrowski Date: Mon, 23 Feb 2026 18:04:53 +0000 Subject: [PATCH 04/10] Update hacs.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- hacs.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index 65800ed..9046f02 100644 --- a/hacs.json +++ b/hacs.json @@ -1,4 +1,6 @@ { "name": "PyTap", - "render_readme": true + "render_readme": true, + "filename": "custom_components/pytap", + "homeassistant": "2023.9.0" } \ No newline at end of file From 573ecc62fccce3785e86ab101855ae87107ed95d Mon Sep 17 00:00:00 2001 From: Adam Zebrowski Date: Mon, 23 Feb 2026 18:05:11 +0000 Subject: [PATCH 05/10] Update .github/workflows/release.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3d189e..fac2c89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Create release archive run: | cd custom_components/pytap - zip -r ../../pytap.zip . -x '__pycache__/*' '*/__pycache__/*' '.pytest_cache/*' + zip -r ../../pytap.zip . -x '__pycache__/*' '*/__pycache__/*' '.pytest_cache/*' '*/.pytest_cache/*' - name: Upload release asset uses: actions/upload-release-asset@v1 From bdd4aa62d4c4b096d13858242e67471e32d5025b Mon Sep 17 00:00:00 2001 From: Adam Zebrowski Date: Mon, 23 Feb 2026 18:05:25 +0000 Subject: [PATCH 06/10] Update .github/workflows/release.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/release.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fac2c89..7bad924 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,11 +21,6 @@ jobs: zip -r ../../pytap.zip . -x '__pycache__/*' '*/__pycache__/*' '.pytest_cache/*' '*/.pytest_cache/*' - name: Upload release asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@v1 with: - upload_url: ${{ github.event.release.upload_url }} - asset_path: ./pytap.zip - asset_name: pytap.zip - asset_content_type: application/zip + files: ./pytap.zip 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 07/10] 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 08/10] 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"] From 94d53c62cd225cd9c3ea5cc9fbc2fdddf5fd53f8 Mon Sep 17 00:00:00 2001 From: Adam Zebrowski Date: Mon, 23 Feb 2026 18:55:23 +0000 Subject: [PATCH 09/10] fix: simplify test requirements to avoid dependency conflicts Remove strict pins for pytest, pytest-asyncio, pytest-socket, pytest-timeout that conflict with pytest-homeassistant-custom-component's own dependency tree. --- requirements-test.txt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index e6d8224..3434e8a 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,7 +1,5 @@ # Test dependencies -pytest==9.0.0 -pytest-asyncio==1.3.0 +# pytest-homeassistant-custom-component pulls in pytest, pytest-asyncio, +# and all HA core dependencies transitively. pytest-homeassistant-custom-component==0.13.315 pytest_freezer==0.4.9 -pytest-socket==0.7.0 -pytest-timeout==2.4.0 From bfdae6d1e7a3857053dbf2cd3bd11a20d20a0302 Mon Sep 17 00:00:00 2001 From: Adam Zebrowski Date: Mon, 23 Feb 2026 18:58:10 +0000 Subject: [PATCH 10/10] fix: use Python 3.13 in CI workflows pytest-homeassistant-custom-component>=0.13.315 requires Python >=3.13, matching HA core 2026.2+ requirements. --- .github/workflows/validate.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b859b9c..f3c858e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -34,7 +34,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.13" - run: pip install ruff - run: ruff check custom_components/pytap/ @@ -45,6 +45,6 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.13" - run: pip install -r requirements-test.txt - run: python -m pytest tests/ -vv --tb=short