diff --git a/.ruff.toml b/.ruff.toml index e807fe9..c24d285 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -22,6 +22,7 @@ ignore = [ "D203", # no-blank-line-before-class (incompatible with formatter) "D212", # multi-line-summary-first-line (incompatible with formatter) "D213", # Multi-line docstring summary should start at the second line + "E501", # line too long "ISC001", # incompatible with formatter "PLR0911", # Too many return statements ({returns} > {max_returns}) diff --git a/custom_components/zaptec/binary_sensor.py b/custom_components/zaptec/binary_sensor.py index 96dabd0..61c6476 100644 --- a/custom_components/zaptec/binary_sensor.py +++ b/custom_components/zaptec/binary_sensor.py @@ -12,11 +12,10 @@ BinarySensorEntityDescription, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ZaptecBaseEntity -from .manager import ZaptecConfigEntry +from .manager import ZaptecConfigEntry, ZaptecEntityDescription _LOGGER = logging.getLogger(__name__) @@ -44,13 +43,13 @@ def _post_init(self) -> None: @dataclass(frozen=True, kw_only=True) -class ZapBinarySensorEntityDescription(BinarySensorEntityDescription): +class ZapBinarySensorEntityDescription(ZaptecEntityDescription, BinarySensorEntityDescription): """Class describing Zaptec binary sensor entities.""" cls: type[BinarySensorEntity] -INSTALLATION_ENTITIES: list[EntityDescription] = [ +INSTALLATION_ENTITIES: list[ZaptecEntityDescription] = [ ZapBinarySensorEntityDescription( key="active", name="Installation", # Special case, no translation @@ -72,7 +71,7 @@ class ZapBinarySensorEntityDescription(BinarySensorEntityDescription): ), ] -CHARGER_ENTITIES: list[EntityDescription] = [ +CHARGER_ENTITIES: list[ZaptecEntityDescription] = [ ZapBinarySensorEntityDescription( key="active", name="Charger", # Special case, no translation diff --git a/custom_components/zaptec/button.py b/custom_components/zaptec/button.py index fbb17af..3cf66fa 100644 --- a/custom_components/zaptec/button.py +++ b/custom_components/zaptec/button.py @@ -9,11 +9,10 @@ from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ZaptecBaseEntity -from .manager import ZaptecConfigEntry +from .manager import ZaptecConfigEntry, ZaptecEntityDescription from .zaptec import Charger _LOGGER = logging.getLogger(__name__) @@ -46,15 +45,15 @@ async def async_press(self) -> None: @dataclass(frozen=True, kw_only=True) -class ZapButtonEntityDescription(ButtonEntityDescription): +class ZapButtonEntityDescription(ZaptecEntityDescription, ButtonEntityDescription): """Class describing Zaptec button entities.""" cls: type[ButtonEntity] -INSTALLATION_ENTITIES: list[EntityDescription] = [] +INSTALLATION_ENTITIES: list[ZaptecEntityDescription] = [] -CHARGER_ENTITIES: list[EntityDescription] = [ +CHARGER_ENTITIES: list[ZaptecEntityDescription] = [ ZapButtonEntityDescription( key="resume_charging", translation_key="resume_charging", diff --git a/custom_components/zaptec/diagnostics.py b/custom_components/zaptec/diagnostics.py index 263b0e4..a45831b 100644 --- a/custom_components/zaptec/diagnostics.py +++ b/custom_components/zaptec/diagnostics.py @@ -30,6 +30,7 @@ async def async_get_config_entry_diagnostics( return await _get_diagnostics(hass, config_entry) except Exception: _LOGGER.exception("Error getting diagnostics") + return {} async def async_get_device_diagnostics( @@ -40,6 +41,7 @@ async def async_get_device_diagnostics( return await _get_diagnostics(hass, config_entry) except Exception: _LOGGER.exception("Error getting diagnostics for device %s", device.id) + return {} async def _get_diagnostics( @@ -80,23 +82,24 @@ def add_failure(err: Exception) -> None: try: api = out.setdefault("api", {}) - async def request(url: str) -> Any: + async def request(url: str) -> dict | list: """Make an API request and return the result.""" try: result = await zaptec.request(url) if not isinstance(result, (dict, list)): return { - "type error": f"Expected dict, got type {type(result).__name__}, value {result}", + "type error": f"Expected dict or list, got type {type(result).__name__}, value {result}", } - return result except Exception as err: return { "exception": type(err).__name__, "err": str(err), "tb": list(traceback.format_exc().splitlines()), } + else: + return result - def add(url, obj, ctx=None) -> None: + def add(url: str, obj: dict | list, ctx: str = "") -> None: api[redact(url, ctx=ctx)] = redact(obj, ctx=ctx) data = await request(url := "installation") @@ -109,8 +112,9 @@ def add(url, obj, ctx=None) -> None: for circuit in data.get("Circuits", []): add(f"circuits/{circuit['Id']}", circuit, ctx="circuit") - for charger in circuit.get("Chargers", []): - charger_in_circuits_ids.append(charger["Id"]) + charger_in_circuits_ids.extend( + charger["Id"] for charger in circuit.get("Chargers", []) + ) add(url, data, ctx="hierarchy") @@ -126,7 +130,7 @@ def add(url, obj, ctx=None) -> None: add(url, data, ctx="charger") data = await request(url := f"chargers/{charger_id}/state") - redact.redact_statelist(data, ctx="state") + data = redact.redact_statelist(data, ctx="state") add(url, data, ctx="state") except Exception as err: diff --git a/custom_components/zaptec/manager.py b/custom_components/zaptec/manager.py index ff62b8f..caa2b2a 100644 --- a/custom_components/zaptec/manager.py +++ b/custom_components/zaptec/manager.py @@ -146,8 +146,8 @@ def create_entities_from_descriptions( def create_entities_from_zaptec( self, - installation_descriptions: Iterable[EntityDescription], - charger_descriptions: Iterable[EntityDescription], + installation_descriptions: Iterable[ZaptecEntityDescription], + charger_descriptions: Iterable[ZaptecEntityDescription], ) -> list[ZaptecBaseEntity]: """Create entities from the present Zaptec objects. diff --git a/custom_components/zaptec/number.py b/custom_components/zaptec/number.py index 8ae456b..f91880a 100644 --- a/custom_components/zaptec/number.py +++ b/custom_components/zaptec/number.py @@ -14,11 +14,10 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ZaptecBaseEntity -from .manager import ZaptecConfigEntry +from .manager import ZaptecConfigEntry, ZaptecEntityDescription from .zaptec import Charger, Installation _LOGGER = logging.getLogger(__name__) @@ -108,6 +107,8 @@ def _post_init(self) -> None: async def async_set_native_value(self, value: float) -> None: """Update to Zaptec.""" self._log_number(value) + if not self.entity_description.setting: + raise HomeAssistantError(f"No setting for {self.__class__.__qualname__}.{self.key}") try: await self.zaptec_obj.set_settings({self.entity_description.setting: value}) except Exception as exc: @@ -144,14 +145,14 @@ async def async_set_native_value(self, value: float) -> None: @dataclass(frozen=True, kw_only=True) -class ZapNumberEntityDescription(NumberEntityDescription): +class ZapNumberEntityDescription(ZaptecEntityDescription, NumberEntityDescription): """Class describing Zaptec number entities.""" cls: type[NumberEntity] setting: str | None = None -INSTALLATION_ENTITIES: list[EntityDescription] = [ +INSTALLATION_ENTITIES: list[ZaptecEntityDescription] = [ ZapNumberEntityDescription( key="available_current", translation_key="available_current", @@ -174,7 +175,7 @@ class ZapNumberEntityDescription(NumberEntityDescription): ), ] -CHARGER_ENTITIES: list[EntityDescription] = [ +CHARGER_ENTITIES: list[ZaptecEntityDescription] = [ ZapNumberEntityDescription( key="charger_min_current", translation_key="charger_min_current", diff --git a/custom_components/zaptec/sensor.py b/custom_components/zaptec/sensor.py index f219ac7..79645e9 100644 --- a/custom_components/zaptec/sensor.py +++ b/custom_components/zaptec/sensor.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, replace import logging -from typing import ClassVar +from typing import Final from homeassistant import const from homeassistant.components.sensor import ( @@ -14,11 +14,10 @@ SensorStateClass, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ZaptecBaseEntity -from .manager import ZaptecConfigEntry +from .manager import ZaptecConfigEntry, ZaptecEntityDescription from .zaptec import ZCONST, get_ocmf_max_reader_value _LOGGER = logging.getLogger(__name__) @@ -76,7 +75,7 @@ class ZaptecChargeSensor(ZaptecSensorTranslate): _log_attribute = "_attr_native_value" # See ZCONST.charger_operation_modes for possible values - CHARGE_MODE_ICON_MAP: ClassVar[dict[str, str]] = { + CHARGE_MODE_ICON_MAP: Final[dict[str, str]] = { "unknown": "mdi:help-rhombus-outline", "disconnected": "mdi:power-plug-off", "connected_requesting": "mdi:timer-sand", @@ -100,7 +99,7 @@ class ZaptecEnengySensor(ZaptecSensor): _log_attribute = "_attr_native_value" # This entity use several attributes from Zaptec - _log_zaptec_key = ["signed_meter_value", "completed_session"] + _log_zaptec_key: Final = ["signed_meter_value", "completed_session"] @callback def _update_from_zaptec(self) -> None: @@ -131,13 +130,13 @@ def _update_from_zaptec(self) -> None: @dataclass(frozen=True, kw_only=True) -class ZapSensorEntityDescription(SensorEntityDescription): +class ZapSensorEntityDescription(ZaptecEntityDescription, SensorEntityDescription): """Provide a description of a Zaptec sensor.""" cls: type[SensorEntity] -INSTALLATION_ENTITIES: list[EntityDescription] = [ +INSTALLATION_ENTITIES: list[ZaptecEntityDescription] = [ ZapSensorEntityDescription( key="available_current_phase1", translation_key="available_current_phase1", @@ -207,7 +206,7 @@ class ZapSensorEntityDescription(SensorEntityDescription): ), ] -CHARGER_ENTITIES: list[EntityDescription] = [ +CHARGER_ENTITIES: list[ZaptecEntityDescription] = [ ZapSensorEntityDescription( key="charger_operation_mode", translation_key="charger_operation_mode", diff --git a/custom_components/zaptec/services.py b/custom_components/zaptec/services.py index f7b7a37..ac406ca 100644 --- a/custom_components/zaptec/services.py +++ b/custom_components/zaptec/services.py @@ -116,7 +116,7 @@ ) -async def async_setup_services(hass: HomeAssistant, manager: ZaptecManager) -> None: +async def async_setup_services(hass: HomeAssistant, manager: ZaptecManager) -> None: # noqa: C901 Too complex, will be fixed in https://github.com/custom-components/zaptec/issues/253 """Set up services for zaptec.""" def get_as_set(service_call: ServiceCall, key: str) -> set[str]: @@ -127,7 +127,7 @@ def get_as_set(service_call: ServiceCall, key: str) -> set[str]: def iter_objects( service_call: ServiceCall, mustbe: type[T] - ) -> Generator[tuple[ZaptecUpdateCoordinator, T], None, None]: + ) -> Generator[tuple[ZaptecUpdateCoordinator, T]]: ent_reg = er.async_get(hass) dev_reg = dr.async_get(hass) @@ -306,6 +306,8 @@ async def service_handle_send_command(service_call: ServiceCall) -> None: for coordinator, obj in iter_objects(service_call, mustbe=Charger): _LOGGER.debug(" >> to %s", obj.id) command = service_call.data.get("command") + if command is None: + raise HomeAssistantError("No Command received") try: await obj.command(command) except Exception as exc: diff --git a/custom_components/zaptec/switch.py b/custom_components/zaptec/switch.py index 5a28ff2..6d87f77 100644 --- a/custom_components/zaptec/switch.py +++ b/custom_components/zaptec/switch.py @@ -14,11 +14,10 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ZaptecBaseEntity -from .manager import ZaptecConfigEntry +from .manager import ZaptecConfigEntry, ZaptecEntityDescription from .zaptec import Charger _LOGGER = logging.getLogger(__name__) @@ -126,15 +125,15 @@ async def async_turn_on(self, **kwargs: Any) -> None: @dataclass(frozen=True, kw_only=True) -class ZapSwitchEntityDescription(SwitchEntityDescription): +class ZapSwitchEntityDescription(ZaptecEntityDescription, SwitchEntityDescription): """Class describing Zaptec switch entities.""" cls: type[SwitchEntity] -INSTALLATION_ENTITIES: list[EntityDescription] = [] +INSTALLATION_ENTITIES: list[ZaptecEntityDescription] = [] -CHARGER_ENTITIES: list[EntityDescription] = [ +CHARGER_ENTITIES: list[ZaptecEntityDescription] = [ ZapSwitchEntityDescription( key="charger_operation_mode", translation_key="charger_operation_mode", diff --git a/custom_components/zaptec/update.py b/custom_components/zaptec/update.py index 93b97a6..e458167 100644 --- a/custom_components/zaptec/update.py +++ b/custom_components/zaptec/update.py @@ -4,7 +4,7 @@ from dataclasses import dataclass import logging -from typing import Any +from typing import Any, Final from homeassistant import const from homeassistant.components.update import ( @@ -14,11 +14,10 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import ZaptecBaseEntity -from .manager import ZaptecConfigEntry +from .manager import ZaptecConfigEntry, ZaptecEntityDescription from .zaptec import Charger _LOGGER = logging.getLogger(__name__) @@ -30,7 +29,7 @@ class ZaptecUpdate(ZaptecBaseEntity, UpdateEntity): # What to log on entity update _log_attribute = "_attr_installed_version" # This entity use several attributes from Zaptec - _log_zaptec_key = ["firmware_current_version", "firmware_available_version"] + _log_zaptec_key: Final = ["firmware_current_version", "firmware_available_version"] zaptec_obj: Charger @callback @@ -58,15 +57,15 @@ async def async_install(self, version: str | None, backup: bool, **kwargs: Any) @dataclass(frozen=True, kw_only=True) -class ZapUpdateEntityDescription(UpdateEntityDescription): +class ZapUpdateEntityDescription(ZaptecEntityDescription, UpdateEntityDescription): """Class describing Zaptec update entities.""" cls: type[UpdateEntity] -INSTALLATION_ENTITIES: list[EntityDescription] = [] +INSTALLATION_ENTITIES: list[ZaptecEntityDescription] = [] -CHARGER_ENTITIES: list[EntityDescription] = [ +CHARGER_ENTITIES: list[ZaptecEntityDescription] = [ ZapUpdateEntityDescription( key="firmware_update", translation_key="firmware_update", diff --git a/custom_components/zaptec/zaptec/api.py b/custom_components/zaptec/zaptec/api.py index cbd67da..618a35f 100644 --- a/custom_components/zaptec/zaptec/api.py +++ b/custom_components/zaptec/zaptec/api.py @@ -30,6 +30,7 @@ API_TIMEOUT, API_URL, CHARGER_EXCLUDES, + DEFAULT_MAX_CURRENT, MAX_DEBUG_TEXT_LEN_ON_500, MISSING, TOKEN_URL, @@ -63,7 +64,7 @@ class TLogExc(Protocol): """Protocol for logging exceptions.""" - def __call__(self, exc: Exception) -> Exception: ... + def __call__(self, exc: Exception) -> Exception: ... # noqa: D102 HA core ignores this, not sure how class ZaptecBase(Mapping[str, TValue]): @@ -96,12 +97,12 @@ def __iter__(self) -> Iterator[str]: @property def id(self) -> str: """Return the id of the object.""" - return self._attrs["id"] + return str(self._attrs["id"]) @property def name(self) -> str: """Return the name of the object.""" - return self._attrs["name"] + return str(self._attrs["name"]) @property def qual_id(self) -> str: @@ -116,7 +117,7 @@ def model(self) -> str: """Return the model of the object.""" return f"Zaptec {self.__class__.__qualname__}" - def asdict(self): + def asdict(self) -> TDict: """Return the attributes as a dict.""" return self._attrs @@ -186,13 +187,13 @@ def state_to_attrs( key: str, keydict: dict[str, str], excludes: set[str] = set(), - ): + ) -> dict[str, str]: """Convert a list of state data into a dict of attributes. `key` is the key that specifies the attribute name. `keydict` is a dict that maps the key value to an attribute name. """ - out = {} + out: dict[str, str] = {} for item in data: skey = item.get(key) if skey is None: @@ -231,7 +232,7 @@ def __init__(self, data: TDict, zaptec: Zaptec) -> None: self._stream_receiver = None self._stream_running = False - async def build(self): + async def build(self) -> None: """Build the installation object hierarchy.""" # Get the hierarchy of circurits and chargers @@ -325,7 +326,7 @@ async def poll_firmware_info(self) -> None: # If the charger is already added to the Zaptec platform but not yet # initialized, these fields are not available. _LOGGER.warning( - "Missing firmware info for charger %s because the charger hasn't been initialized yet. Safe to ignore.", # noqa: E501 + "Missing firmware info for charger %s because the charger hasn't been initialized yet. Safe to ignore.", charger.qual_id, ) continue @@ -389,7 +390,8 @@ async def stream_main(self, cb=None, ssl_context=None) -> None: if err.error_code != HTTPStatus.FORBIDDEN: raise _LOGGER.warning( - "Failed to get live stream info. Check if user have access in the zaptec portal" + "Failed to get live stream info. " + "Check if user have access in the zaptec portal" ) return @@ -542,11 +544,10 @@ async def set_limit_current(self, **kwargs): "availableCurrentPhase3 are set, then all of them must be set" ) - # Use 32 as default if missing or invalid value. try: - max_current = float(self.get("max_current", 32.0)) + max_current = float(self.get("max_current", DEFAULT_MAX_CURRENT)) except (TypeError, ValueError): - max_current = 32.0 + max_current = DEFAULT_MAX_CURRENT # Make sure the arguments and values are valid for k, v in kwargs.items(): if k not in ( @@ -560,21 +561,19 @@ async def set_limit_current(self, **kwargs): raise ValueError(f"{k} cannot be None") if not (0 <= v <= max_current): raise ValueError(f"{k} must be between 0 and {max_current:.0f} amps") - data = await self.zaptec.request( + return await self.zaptec.request( f"installation/{self.id}/update", method="post", data=kwargs ) - return data async def set_three_to_one_phase_switch_current(self, current: float): """Set the 3 to 1-phase switch current.""" - if not (0 <= current <= 32): - raise ValueError("Current must be between 0 and 32 amps") - data = await self.zaptec.request( + if not (0 <= current <= DEFAULT_MAX_CURRENT): + raise ValueError(f"Current must be between 0 and {DEFAULT_MAX_CURRENT:.0f} amps") + return await self.zaptec.request( f"installation/{self.id}/update", method="post", data={"threeToOnePhaseSwitchCurrent": current}, ) - return data class Charger(ZaptecBase): @@ -699,8 +698,7 @@ async def command(self, command: str | int | CommandType): self.is_command_valid(command, raise_value_error_if_invalid=True) _LOGGER.debug("Command %s (%s)", command, cmdid) - data = await self.zaptec.request(f"chargers/{self.id}/SendCommand/{cmdid}", method="post") - return data + return await self.zaptec.request(f"chargers/{self.id}/SendCommand/{cmdid}", method="post") def is_command_valid(self, command: str, raise_value_error_if_invalid: bool = False) -> bool: """Check if the command is valid.""" @@ -736,21 +734,19 @@ def is_command_valid(self, command: str, raise_value_error_if_invalid: bool = Fa async def set_settings(self, settings: dict[str, Any]): """Set settings on the charger.""" - if any(key not in ZCONST.update_params for key in settings.keys()): + if any(key not in ZCONST.update_params for key in settings): raise ValueError(f"Unknown setting '{settings}'") _LOGGER.debug("Settings %s", settings) - data = await self.zaptec.request( + return await self.zaptec.request( f"chargers/{self.id}/update", method="post", data=settings ) - return data async def authorize_charge(self): """Authorize the charger to charge.""" _LOGGER.debug("Authorize charge") # NOTE: Undocumented API call - data = await self.zaptec.request(f"chargers/{self.id}/authorizecharge", method="post") - return data + return await self.zaptec.request(f"chargers/{self.id}/authorizecharge", method="post") async def set_permanent_cable_lock(self, lock: bool): """Set the permanent cable lock on the charger.""" @@ -761,10 +757,9 @@ async def set_permanent_cable_lock(self, lock: bool): }, } # NOTE: Undocumented API call - result = await self.zaptec.request( + return await self.zaptec.request( f"chargers/{self.id}/localSettings", method="post", data=data ) - return result async def set_hmi_brightness(self, brightness: float): """Set the HMI brightness.""" @@ -775,10 +770,9 @@ async def set_hmi_brightness(self, brightness: float): }, } # NOTE: Undocumented API call - result = await self.zaptec.request( + return await self.zaptec.request( f"chargers/{self.id}/localSettings", method="post", data=data ) - return result def is_charging(self) -> bool: """Check if the charger is charging.""" @@ -849,9 +843,9 @@ async def __aexit__(self, exc_type, exc_value, traceback): # ======================================================================= # MAPPING METHODS - def __getitem__(self, id: str) -> ZaptecBase: + def __getitem__(self, obj_id: str) -> ZaptecBase: """Get an object data by id.""" - return self._map[id] + return self._map[obj_id] def __iter__(self) -> Iterator[str]: """Return an iterator over the object ids.""" @@ -868,17 +862,18 @@ def __contains__(self, key: str | ZaptecBase) -> bool: return any(obj is key for obj in self._map.values()) return key in self._map - def register(self, id: str, data: ZaptecBase) -> None: + def register(self, obj_id: str, data: ZaptecBase) -> None: """Register an object data with id.""" - if id in self._map: + if obj_id in self._map: raise ValueError( - f"Object with id {id} already registered. Use unregister() to remove it first." + f"Object with id {obj_id} already registered. " + "Use unregister() to remove it first." ) - self._map[id] = data + self._map[obj_id] = data - def unregister(self, id: str) -> None: + def unregister(self, obj_id: str) -> None: """Unregister an object data with id.""" - del self._map[id] + del self._map[obj_id] def objects(self) -> Iterable[ZaptecBase]: """Return an iterable of all registered objects.""" @@ -894,21 +889,21 @@ def chargers(self) -> Iterable[Charger]: """Return a list of all chargers.""" return [v for v in self._map.values() if isinstance(v, Charger)] - def qual_id(self, id: str) -> str: + def qual_id(self, obj_id: str) -> str: """Get the qualified id of an object. If the object is not found, return the id as is. """ - obj = self._map.get(id) + obj = self._map.get(obj_id) if obj is None: - return id + return obj_id return obj.qual_id # ======================================================================= # REQUEST METHODS @staticmethod - def _request_log(url, method, iteration, **kwargs): + def _request_log(url: str, method: str, iteration: int, **kwargs): """Helper that yields request log entries.""" try: data = kwargs.get("data", "") @@ -924,7 +919,7 @@ def _request_log(url, method, iteration, **kwargs): # Remove the Authorization header from the log if "Authorization" in headers: headers["Authorization"] = "" - yield f" headers {dict((k, v) for k, v in headers.items())}" + yield f" headers '{headers}'" if "data" in kwargs: yield f" data '{kwargs['data']}'" if "json" in kwargs: @@ -940,7 +935,7 @@ async def _response_log(resp: aiohttp.ClientResponse): yield f"@@@ RESPONSE {resp.status} length {len(contents)}" if not DEBUG_API_DATA: return - yield f" headers {dict((k, v) for k, v in resp.headers.items())}" + yield f" headers '{resp.headers}'" if not contents: return if resp.status != HTTPStatus.OK: @@ -951,7 +946,7 @@ async def _response_log(resp: aiohttp.ClientResponse): _LOGGER.exception("Failed to log response (ignored exception)") async def _request_worker( - self, url: str, method="get", retries=API_RETRIES, **kwargs + self, url: str, method: str = "get", retries: int = API_RETRIES, **kwargs ) -> AsyncGenerator[tuple[aiohttp.ClientResponse, TLogExc], None]: """API request generator that handles retries. @@ -1009,7 +1004,7 @@ def log_exc(exc: Exception) -> Exception: yield response, log_exc # Exceptions that can be retried - except (asyncio.TimeoutError, aiohttp.ClientConnectionError) as err: + except (TimeoutError, aiohttp.ClientConnectionError) as err: error = err # Capture tha last error if DEBUG_API_EXCEPTIONS: _LOGGER.error( @@ -1030,7 +1025,7 @@ def log_exc(exc: Exception) -> Exception: # longer than the calculated delay, so we don't need to sleep. sleep_delay = delay - time.perf_counter() + start_time - if isinstance(error, asyncio.TimeoutError): + if isinstance(error, TimeoutError): raise RequestTimeoutError( f"Request to {url} timed out after {iteration} retries" ) from None diff --git a/custom_components/zaptec/zaptec/const.py b/custom_components/zaptec/zaptec/const.py index ee9c41c..6f55ec2 100644 --- a/custom_components/zaptec/zaptec/const.py +++ b/custom_components/zaptec/zaptec/const.py @@ -42,6 +42,12 @@ class Missing: MAX_DEBUG_TEXT_LEN_ON_500 = 150 """Maximum text length to add to debug log without truncating.""" +DEFAULT_MIN_CURRENT = 6.0 +"""The lowest supported current for EV charging according to IEC standards.""" + +DEFAULT_MAX_CURRENT = 32.0 +"""The default max_current to use if max_current is missing or invalid.""" + TRUTHY = ["true", "1", "on", "yes", 1, True] FALSY = ["false", "0", "off", "no", 0, False] diff --git a/custom_components/zaptec/zaptec/validate.py b/custom_components/zaptec/zaptec/validate.py index dcbf4a2..2177333 100644 --- a/custom_components/zaptec/zaptec/validate.py +++ b/custom_components/zaptec/zaptec/validate.py @@ -46,7 +46,7 @@ class ChargerState(BaseModel): model_config = ConfigDict(extra="allow") StateId: int - # ValueAsString: str # StateId -1 (Pulse) does not include a ValueAsString-field # noqa: E501, ERA001 + # ValueAsString: str # StateId -1 (Pulse) does not include a ValueAsString-field # noqa: ERA001 class Chargers(BaseModel):