fix: linting and formatting

This commit is contained in:
Richard Mann
2026-04-06 15:13:21 +10:00
parent af08d18d62
commit 652337e32c
6 changed files with 128 additions and 100 deletions

View File

@@ -228,7 +228,7 @@ class PowerViewBLE:
await self._cmd( await self._cmd(
( (
ShadeCmd.SET_POSITION, ShadeCmd.SET_POSITION,
int.to_bytes(pos1*100, 2, byteorder="little") int.to_bytes(pos1 * 100, 2, byteorder="little")
+ int.to_bytes(pos2, 2, byteorder="little") + int.to_bytes(pos2, 2, byteorder="little")
+ int.to_bytes(pos3, 2, byteorder="little") + int.to_bytes(pos3, 2, byteorder="little")
+ int.to_bytes(tilt, 2, byteorder="little") + int.to_bytes(tilt, 2, byteorder="little")

View File

@@ -40,7 +40,9 @@ async def async_setup_entry(
) )
class PVBinarySensor(PassiveBluetoothCoordinatorEntity[PVCoordinator], BinarySensorEntity): # type: ignore[reportIncompatibleMethodOverride] class PVBinarySensor(
PassiveBluetoothCoordinatorEntity[PVCoordinator], BinarySensorEntity
): # type: ignore[reportIncompatibleMethodOverride]
"""The generic PV binary sensor implementation.""" """The generic PV binary sensor implementation."""
def __init__( def __init__(

View File

@@ -2,8 +2,8 @@
import asyncio import asyncio
import base64 import base64
import struct
from dataclasses import dataclass from dataclasses import dataclass
import struct
from typing import Any from typing import Any
import aiohttp import aiohttp
@@ -114,6 +114,9 @@ async def _fetch_key_and_shades_from_hub(
) as resp: ) as resp:
resp.raise_for_status() resp.raise_for_status()
result = await resp.json(content_type=None) result = await resp.json(content_type=None)
except (TimeoutError, aiohttp.ClientError) as ex:
last_error = ex
continue
responses = result.get("responses", []) responses = result.get("responses", [])
if len(responses) != 1 or "hex" not in responses[0]: if len(responses) != 1 or "hex" not in responses[0]:
@@ -131,9 +134,6 @@ async def _fetch_key_and_shades_from_hub(
if len(key_data) != 16: if len(key_data) != 16:
continue continue
return key_data, hub_shades return key_data, hub_shades
except (aiohttp.ClientError, asyncio.TimeoutError) as ex:
last_error = ex
continue
raise ValueError(f"No reachable shade returned a valid key: {last_error}") raise ValueError(f"No reachable shade returned a valid key: {last_error}")
@@ -205,6 +205,27 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data=data, data=data,
) )
def _validate_manual_key(
self, user_input: dict[str, Any], errors: dict[str, str]
) -> bool:
"""Validate a manually entered hex key and store it.
Returns True on success, False on validation error.
"""
raw = user_input.get("home_key", "").strip()
if "\\x" in raw:
raw = raw.replace("\\x", "")
if len(raw) != 32:
errors["home_key"] = "invalid_key_length"
return False
try:
bytes.fromhex(raw)
except ValueError:
errors["home_key"] = "invalid_key_format"
return False
self._home_key = raw.lower()
return True
async def _validate_homekey_input( async def _validate_homekey_input(
self, user_input: dict[str, Any], errors: dict[str, str] self, user_input: dict[str, Any], errors: dict[str, str]
) -> bool: ) -> bool:
@@ -220,40 +241,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return True return True
if method == "manual": if method == "manual":
raw = user_input.get("home_key", "").strip() return self._validate_manual_key(user_input, errors)
if "\\x" in raw:
raw = raw.replace("\\x", "") if method != "hub":
if len(raw) != 32: return False
errors["home_key"] = "invalid_key_length"
return False
try:
bytes.fromhex(raw)
except ValueError:
errors["home_key"] = "invalid_key_format"
return False
self._home_key = raw.lower()
return True
if method == "hub":
hub_url = user_input.get("hub_url", "").rstrip("/") hub_url = user_input.get("hub_url", "").rstrip("/")
_HUB_ERROR_MAP: dict[type[Exception], str] = {
aiohttp.ClientResponseError: "hub_http_error",
aiohttp.ClientConnectionError: "hub_connection_error",
TimeoutError: "hub_timeout",
ValueError: "hub_protocol_error",
}
try: try:
key, hub_shades = await _fetch_key_and_shades_from_hub( key, hub_shades = await _fetch_key_and_shades_from_hub(self.hass, hub_url)
self.hass, hub_url except tuple(_HUB_ERROR_MAP) as ex:
) errors["hub_url"] = _HUB_ERROR_MAP[type(ex)]
return False
self._home_key = key.hex() self._home_key = key.hex()
self._hub_url = hub_url self._hub_url = hub_url
self._hub_shades = hub_shades self._hub_shades = hub_shades
return True return True
except aiohttp.ClientResponseError:
errors["hub_url"] = "hub_http_error"
except aiohttp.ClientConnectionError:
errors["hub_url"] = "hub_connection_error"
except (asyncio.TimeoutError, TimeoutError):
errors["hub_url"] = "hub_timeout"
except ValueError:
errors["hub_url"] = "hub_protocol_error"
return False
async def async_step_bluetooth( async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak self, discovery_info: BluetoothServiceInfoBleak
@@ -310,8 +319,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None and await self._validate_homekey_input(
if await self._validate_homekey_input(user_input, errors): user_input, errors
):
# Use hub name for the entry title if available # Use hub name for the entry title if available
friendly = self._hub_name_for(self._device_name) friendly = self._hub_name_for(self._device_name)
if friendly: if friendly:
@@ -349,7 +359,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
break break
if not self._hub_url: if not self._hub_url:
self._hub_url = hub_url self._hub_url = hub_url
except (aiohttp.ClientError, asyncio.TimeoutError, ValueError): except (TimeoutError, aiohttp.ClientError, ValueError):
pass pass
def _hub_name_for(self, ble_name: str) -> str | None: def _hub_name_for(self, ble_name: str) -> str | None:
@@ -370,18 +380,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_select_device() return await self.async_step_select_device()
return await self.async_step_homekey() return await self.async_step_homekey()
async def async_step_select_device( def _build_selected_entries(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any]
) -> ConfigFlowResult: ) -> list[dict[str, Any]]:
"""Select one or more BLE-discovered shades, or fall through to manual.""" """Build config entry data for each selected shade address."""
LOGGER.debug("select_device step")
if user_input is not None:
addresses: list[str] = user_input[CONF_ADDRESS] addresses: list[str] = user_input[CONF_ADDRESS]
if isinstance(addresses, str): if isinstance(addresses, str):
addresses = [addresses] addresses = [addresses]
# Build entry info for every selected shade
entries: list[dict[str, Any]] = [] entries: list[dict[str, Any]] = []
for address in addresses: for address in addresses:
device = self._discovered_devices[address] device = self._discovered_devices[address]
@@ -401,16 +407,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"data": entry_data, "data": entry_data,
} }
) )
return entries
async def async_step_select_device(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Select one or more BLE-discovered shades, or fall through to manual."""
LOGGER.debug("select_device step")
if user_input is not None:
entries = self._build_selected_entries(user_input)
# Kick off auto-add flows for all but the last shade # Kick off auto-add flows for all but the last shade
await asyncio.gather(*( await asyncio.gather(
*(
self.hass.config_entries.flow.async_init( self.hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": "auto_add"}, context={"source": "auto_add"},
data=info, data=info,
) )
for info in entries[:-1] for info in entries[:-1]
)) )
)
# Create the final entry normally (ends this flow) # Create the final entry normally (ends this flow)
last = entries[-1] last = entries[-1]
@@ -518,8 +536,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Configure homekey — collected before device selection.""" """Configure homekey — collected before device selection."""
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None and await self._validate_homekey_input(
if await self._validate_homekey_input(user_input, errors): user_input, errors
):
return await self.async_step_select_device() return await self.async_step_select_device()
return self.async_show_form( return self.async_show_form(

View File

@@ -20,7 +20,10 @@ class PVCoordinator(PassiveBluetoothDataUpdateCoordinator):
"""Update coordinator for a battery management system.""" """Update coordinator for a battery management system."""
def __init__( def __init__(
self, hass: HomeAssistant, ble_device: BLEDevice, data: dict[str, Any], self,
hass: HomeAssistant,
ble_device: BLEDevice,
data: dict[str, Any],
friendly_name: str | None = None, friendly_name: str | None = None,
) -> None: ) -> None:
"""Initialize BMS data coordinator.""" """Initialize BMS data coordinator."""
@@ -28,7 +31,9 @@ class PVCoordinator(PassiveBluetoothDataUpdateCoordinator):
self._mac = ble_device.address self._mac = ble_device.address
self._friendly_name = friendly_name or ble_device.name self._friendly_name = friendly_name or ble_device.name
home_key_hex: str = data.get(CONF_HOME_KEY, "") home_key_hex: str = data.get(CONF_HOME_KEY, "")
home_key: bytes = bytes.fromhex(home_key_hex) if len(home_key_hex) == 32 else b"" home_key: bytes = (
bytes.fromhex(home_key_hex) if len(home_key_hex) == 32 else b""
)
self.api = PowerViewBLE(ble_device, home_key) self.api = PowerViewBLE(ble_device, home_key)
self.data: dict[str, int | float | bool] = {} self.data: dict[str, int | float | bool] = {}
self._manuf_dat = data.get("manufacturer_data") self._manuf_dat = data.get("manufacturer_data")

View File

@@ -34,9 +34,9 @@ async def async_setup_entry(
"""Set up the demo cover platform.""" """Set up the demo cover platform."""
coordinator: PVCoordinator = config_entry.runtime_data coordinator: PVCoordinator = config_entry.runtime_data
model: Final[str|None] = coordinator.dev_details.get("model") model: Final[str | None] = coordinator.dev_details.get("model")
entities: list[PowerViewCover] = [] entities: list[PowerViewCover] = []
if model in ["39"]: if model == "39":
entities.append(PowerViewCoverTiltOnly(coordinator)) entities.append(PowerViewCoverTiltOnly(coordinator))
else: else:
entities.append(PowerViewCover(coordinator)) entities.append(PowerViewCover(coordinator))

View File

@@ -61,7 +61,9 @@ def get_shade_key(hub: str, ble_name) -> bytes:
response: Final[bytes] = bytes.fromhex(responses[0]["hex"]) response: Final[bytes] = bytes.fromhex(responses[0]["hex"])
dec_resp: Final[dict[str, Any]] = decode_response(response) dec_resp: Final[dict[str, Any]] = decode_response(response)
if dec_resp["errorCode"] != 0: if dec_resp["errorCode"] != 0:
raise ValueError(f"BLE errorCode={dec_resp['errorCode']} data={dec_resp['data'].hex()}") raise ValueError(
f"BLE errorCode={dec_resp['errorCode']} data={dec_resp['data'].hex()}"
)
if len(dec_resp["data"]) != 16: if len(dec_resp["data"]) != 16:
raise ValueError("Expected 16 byte homekey") raise ValueError("Expected 16 byte homekey")
return dec_resp["data"] return dec_resp["data"]