fix: linting and formatting
This commit is contained in:
@@ -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__(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ async def async_setup_entry(
|
|||||||
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))
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user