stricter ruff formatting

This commit is contained in:
patman15
2024-11-20 21:38:26 +01:00
parent 9ee1b6c6d2
commit 31352a69b8
8 changed files with 195 additions and 38 deletions

View File

@@ -5,7 +5,6 @@
"""
from bleak.exc import BleakError
from homeassistant.components.bluetooth import async_ble_device_from_address
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform

View File

@@ -1,28 +1,27 @@
"""Hunter Douglas PowerView BLE API."""
import asyncio
import time
from dataclasses import dataclass
from enum import Enum
import time
from typing import Final
from bleak import BleakClient
from bleak.backends.device import BLEDevice
from bleak.exc import BleakError
from bleak.uuids import normalize_uuid_str
from bleak_retry_connector import close_stale_connections, establish_connection
from bleak_retry_connector import establish_connection
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from homeassistant.components.cover import ATTR_CURRENT_POSITION
from .const import LOGGER, TIMEOUT
UUID_COV_SERVICE: Final = normalize_uuid_str("fdc1")
UUID_TX: Final = "cafe1001-c0ff-ee01-8000-a110ca7ab1e0"
UUID_DEV_SERVICE: Final = normalize_uuid_str("180a")
UUID_BAT_SERVICE: Final = normalize_uuid_str("180f")
UUID_COV_SERVICE: Final[str] = normalize_uuid_str("fdc1")
UUID_TX: Final[str] = "cafe1001-c0ff-ee01-8000-a110ca7ab1e0"
UUID_DEV_SERVICE: Final[str] = normalize_uuid_str("180a")
UUID_BAT_SERVICE: Final[str] = normalize_uuid_str("180f")
ATTR_ACTIVITY: Final = "activity"
ATTR_ACTIVITY: Final[str] = "activity"
SHADE_TYPE: Final[dict[int, str]] = {
@@ -82,7 +81,15 @@ class PowerViewBLE:
self._ble_device: Final[BLEDevice] = ble_device
self.name: Final[str] = self._ble_device.name or "unknown"
self._seqcnt: int = 1
self._client: BleakClient | None = None
self._client: BleakClient = BleakClient(
self._ble_device,
disconnected_callback=self._on_disconnect,
services=[
UUID_COV_SERVICE,
# self.UUID_DEV_SERVICE,
# self.UUID_BAT_SERVICE,
],
)
self._data_event = asyncio.Event()
self._data: bytearray
self._info: PVDeviceInfo = PVDeviceInfo()
@@ -106,7 +113,7 @@ class PowerViewBLE:
@property
def is_connected(self) -> bool:
"""Return whether remote device is connected."""
return self._client is not None and self._client.is_connected
return self._client.is_connected
# general cmd: uint16_t cmd, uint8_t seqID, uint8_t data_len
async def _cmd(
@@ -120,7 +127,6 @@ class PowerViewBLE:
async with self._cmd_lock:
try:
await self._connect()
assert self._client is not None, "missing BT client"
cmd_run = self._cmd_next
tx_data = (
bytearray(
@@ -156,7 +162,7 @@ class PowerViewBLE:
LOGGER.debug("not a V2 record!")
return []
pos = int.from_bytes(data[3:5], byteorder="little")
pos2 = ((int(data[5]) << 4) + (int(data[4]) >> 4))
pos2 = (int(data[5]) << 4) + (int(data[4]) >> 4)
return [
(ATTR_CURRENT_POSITION, ((pos >> 2) / 10)),
("position2", pos2 >> 2),
@@ -215,12 +221,12 @@ class PowerViewBLE:
),
)
def _verify_response(self, input: bytearray, seq_nr: int, cmd: ShadeCmd) -> bool:
def _verify_response(self, din: bytearray, seq_nr: int, cmd: ShadeCmd) -> bool:
"""Verify shade response data."""
data = input
data: bytearray = din
if self._cipher is not None:
dec = self._cipher.decryptor()
data = dec.update(input) + dec.finalize()
data = bytearray(dec.update(din) + dec.finalize())
if len(data) < 4:
LOGGER.error("Reponse message too short")
return False
@@ -229,14 +235,14 @@ class PowerViewBLE:
return False
if int(data[2]) != seq_nr:
LOGGER.warning(
f"Response sequence id {int(data[2])} wrong, expected {seq_nr}"
"Response sequence id %i wrong, expected %d", int(data[2]), seq_nr
)
return False
if int(data[3]) != 1:
LOGGER.error("Wrong response data length")
return False
if int(data[4] != 0):
LOGGER.error(f"Command {cmd.value} returned error #{int(data[4])}")
LOGGER.error("Command %d returned error #%d", cmd.value, int(data[4]))
return False
return True
@@ -255,7 +261,6 @@ class PowerViewBLE:
async with self._cmd_lock:
try:
await self._connect()
assert self._client is not None
for key, uuid in uuids.items():
LOGGER.debug("querying %s(%s)", key, uuid)
@@ -265,7 +270,7 @@ class PowerViewBLE:
.decode("UTF-8")
)
finally:
await self._disconnect()
await self.disconnect()
LOGGER.debug("%s device data: %s", self.name, data)
return data.copy()
@@ -289,8 +294,6 @@ class PowerViewBLE:
return
start = time.time()
await close_stale_connections(self._ble_device)
self._client = await establish_connection(
BleakClient,
self._ble_device,
@@ -308,10 +311,10 @@ class PowerViewBLE:
# await self._query_dev_info()
async def _disconnect(self) -> None:
async def disconnect(self) -> None:
"""Disconnect the device and stop notifications."""
if self._client is not None and self.is_connected:
if self.is_connected:
LOGGER.debug("Disconnecting device %s", self.name)
try:
self._data_event.clear()

View File

@@ -4,7 +4,6 @@ from dataclasses import dataclass
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,

View File

@@ -3,9 +3,8 @@
from typing import Any
from bleak.backends.device import BLEDevice
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import DOMAIN as BLUETOOTH_DOMAIN
from homeassistant.components.bluetooth.const import DOMAIN as BLUETOOTH_DOMAIN
from homeassistant.components.bluetooth.passive_update_coordinator import (
PassiveBluetoothDataUpdateCoordinator,
)
@@ -79,6 +78,12 @@ class PVCoordinator(PassiveBluetoothDataUpdateCoordinator):
"""Check if a device is present."""
return bluetooth.async_address_present(self.hass, self._mac, connectable=True)
def _async_stop(self) -> None:
"""Shutdown coordinator and any connection."""
LOGGER.debug("%s: shuting down BMS device", self.name)
self.hass.async_create_task(self.api.disconnect())
super()._async_stop()
@callback
def _async_handle_bluetooth_event(
self,

View File

@@ -3,7 +3,6 @@
from typing import Any, Final
from bleak.exc import BleakError
from homeassistant.components.bluetooth.passive_update_coordinator import (
PassiveBluetoothCoordinatorEntity,
)
@@ -127,7 +126,10 @@ class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEnti
self.async_write_ha_state()
except BleakError as err:
LOGGER.error(
f"Failed to move cover '{self.name}' to {target_position}%: {err}"
"Failed to move cover '%s' to %f%%: %s",
self.name,
target_position,
err,
)
def _reset_target_position(self) -> None:
@@ -143,7 +145,7 @@ class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEnti
await self._coord.api.open()
self.async_write_ha_state()
except BleakError as err:
LOGGER.error(f"Failed to open cover '{self.name}': {err}")
LOGGER.error("Failed to open cover '%s': %s", self.name, err)
self._reset_target_position()
async def async_close_cover(self, **kwargs: Any) -> None:
@@ -156,7 +158,7 @@ class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEnti
await self._coord.api.close()
self.async_write_ha_state()
except BleakError as err:
LOGGER.error(f"Failed to close cover '{self.name}': {err}")
LOGGER.error("Failed to close cover '%s': %s", self.name, err)
self._reset_target_position()
async def async_stop_cover(self, **kwargs: Any) -> None:
@@ -167,4 +169,4 @@ class PowerViewCover(PassiveBluetoothCoordinatorEntity[PVCoordinator], CoverEnti
self._reset_target_position()
self.async_write_ha_state()
except BleakError as err:
LOGGER.error(f"Failed to stop cover '{self.name}': {err}")
LOGGER.error("Failed to stop cover '%s': %s", self.name, err)

View File

@@ -4,9 +4,11 @@ from homeassistant.components.bluetooth.passive_update_coordinator import (
PassiveBluetoothCoordinatorEntity,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.components.sensor.const import (
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.const import (

View File

@@ -1,12 +1,11 @@
# pyproject.toml
#[tool.setuptools.packages.find]
#where = ["custom_components/"]
#include = ["bms_ble"]
[project]
requires-python = ">=3.12.0"
[tool.pytest.ini_options]
minversion = "8.0"
addopts="--cov=custom_components.bms_ble --cov-report=term-missing --cov-fail-under=100"
addopts="--cov=custom_components.hunterdouglas_powerview_ble --cov-report=term-missing --cov-fail-under=100"
pythonpath = [
"custom_components.hunterdouglas_powerview_ble",
]
@@ -14,3 +13,151 @@ testpaths = [
"tests",
]
asyncio_mode = "auto"
# ruff configuration taken from HA 2024.11.2 (less ignores)
[tool.ruff]
required-version = ">=0.6.8"
[tool.ruff.lint]
select = [
"A001", # Variable {name} is shadowing a Python builtin
"ASYNC210", # Async functions should not call blocking HTTP methods
"ASYNC220", # Async functions should not create subprocesses with blocking methods
"ASYNC221", # Async functions should not run processes with blocking methods
"ASYNC222", # Async functions should not wait on processes with blocking methods
"ASYNC230", # Async functions should not open files with blocking methods like open
"ASYNC251", # Async functions should not call time.sleep
"B002", # Python does not support the unary prefix increment
"B005", # Using .strip() with multi-character strings is misleading
"B007", # Loop control variable {name} not used within loop body
"B014", # Exception handler with duplicate exception
"B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it.
"B017", # pytest.raises(BaseException) should be considered evil
"B018", # Found useless attribute access. Either assign it to a variable or remove it.
"B023", # Function definition does not bind loop variable {name}
"B026", # Star-arg unpacking after a keyword argument is strongly discouraged
"B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)?
"B904", # Use raise from to specify exception cause
"B905", # zip() without an explicit strict= parameter
"BLE",
"C", # complexity
"COM818", # Trailing comma on bare tuple prohibited
"D", # docstrings
"DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow()
"DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts)
"E", # pycodestyle
"F", # pyflakes/autoflake
"F541", # f-string without any placeholders
"FLY", # flynt
"FURB", # refurb
"G", # flake8-logging-format
"I", # isort
"INP", # flake8-no-pep420
"ISC", # flake8-implicit-str-concat
"ICN001", # import concentions; {name} should be imported as {asname}
"LOG", # flake8-logging
"N804", # First argument of a class method should be named cls
"N805", # First argument of a method should be named self
"N815", # Variable {name} in class scope should not be mixedCase
"PERF", # Perflint
"PGH", # pygrep-hooks
"PIE", # flake8-pie
"PL", # pylint
"PT", # flake8-pytest-style
"PTH", # flake8-pathlib
"PYI", # flake8-pyi
"RET", # flake8-return
"RSE", # flake8-raise
"RUF005", # Consider iterable unpacking instead of concatenation
"RUF006", # Store a reference to the return value of asyncio.create_task
"RUF010", # Use explicit conversion flag
"RUF013", # PEP 484 prohibits implicit Optional
"RUF017", # Avoid quadratic list summation
"RUF018", # Avoid assignment expressions in assert statements
"RUF019", # Unnecessary key check before dictionary access
# "RUF100", # Unused `noqa` directive; temporarily every now and then to clean them up
"S102", # Use of exec detected
"S103", # bad-file-permissions
"S108", # hardcoded-temp-file
"S306", # suspicious-mktemp-usage
"S307", # suspicious-eval-usage
"S313", # suspicious-xmlc-element-tree-usage
"S314", # suspicious-xml-element-tree-usage
"S315", # suspicious-xml-expat-reader-usage
"S316", # suspicious-xml-expat-builder-usage
"S317", # suspicious-xml-sax-usage
"S318", # suspicious-xml-mini-dom-usage
"S319", # suspicious-xml-pull-dom-usage
"S320", # suspicious-xmle-tree-usage
"S601", # paramiko-call
"S602", # subprocess-popen-with-shell-equals-true
"S604", # call-with-shell-equals-true
"S608", # hardcoded-sql-expression
"S609", # unix-command-wildcard-injection
"SIM", # flake8-simplify
"SLF", # flake8-self
"SLOT", # flake8-slots
"T100", # Trace found: {name} used
"T20", # flake8-print
"TCH", # flake8-type-checking
"TID", # Tidy imports
"TRY", # tryceratops
"UP", # pyupgrade
"UP031", # Use format specifiers instead of percent format
"UP032", # Use f-string instead of `format` call
"W", # pycodestyle
]
ignore = [
"D202", # No blank lines allowed after function docstring
"D203", # 1 blank line required before class docstring
"D213", # Multi-line docstring summary should start at the second line
"D406", # Section name should end with a newline
"D407", # Section name underlining
"E501", # line too long
# "PLC1901", # {existing} can be simplified to {replacement} as an empty string is falsey; too many false positives
# "PLR0911", # Too many return statements ({returns} > {max_returns})
# "PLR0912", # Too many branches ({branches} > {max_branches})
# "PLR0913", # Too many arguments to function call ({c_args} > {max_args})
# "PLR0915", # Too many statements ({statements} > {max_statements})
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
# "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
# "PT004", # Fixture {fixture} does not return anything, add leading underscore
# "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception
# "PT018", # Assertion should be broken down into multiple parts
# "RUF001", # String contains ambiguous unicode character.
# "RUF002", # Docstring contains ambiguous unicode character.
# "RUF003", # Comment contains ambiguous unicode character.
# "RUF015", # Prefer next(...) over single element slice
# "SIM102", # Use a single if statement instead of nested if statements
# "SIM103", # Return the condition {condition} directly
# "SIM108", # Use ternary operator {contents} instead of if-else-block
# "SIM115", # Use context handler for opening files
# Moving imports into type-checking blocks can mess with pytest.patch()
"TCH001", # Move application import {} into a type-checking block
"TCH002", # Move third-party import {} into a type-checking block
"TCH003", # Move standard library import {} into a type-checking block
"TRY003", # Avoid specifying long messages outside the exception class
"TRY400", # Use `logging.exception` instead of `logging.error`
# Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923
"UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)`
# May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
"W191",
"E111",
"E114",
"E117",
"D206",
"D300",
"Q",
"COM812",
"COM819",
"ISC001",
# Disabled because ruff does not understand type of __all__ generated by a function
"PLE0605"
]

View File

@@ -1,4 +1,4 @@
homeassistant==2024.8.0
pip>=21.3.1
ruff==0.4.2
ruff==0.6.8