Skip to content

Commit 21511ad

Browse files
authored
Add more explicit typing (#261)
* Ruff: Ignore type-checking blocks, since HA does too * In additionto the stated reason of messing with pytest.patch, it also just looks messier. * Unnecessary elif after return/continue
1 parent 722589c commit 21511ad

File tree

7 files changed

+49
-41
lines changed

7 files changed

+49
-41
lines changed

.ruff.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ ignore = [
2020
"D212", # multi-line-summary-first-line (incompatible with formatter)
2121
"D213", # Multi-line docstring summary should start at the second line
2222
"ISC001", # incompatible with formatter
23+
24+
# Moving imports into type-checking blocks can mess with pytest.patch()
25+
"TC001", # Move application import {} into a type-checking block
26+
"TC002", # Move third-party import {} into a type-checking block
27+
"TC003", # Move standard library import {} into a type-checking block
28+
2329
"TRY003", # Avoid specifying long messages outside the exception class
2430
"TRY400", # Use `logging.exception` instead of `logging.error`
2531
]

custom_components/zaptec/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ def create_entities_from_zaptec(
487487

488488
return entities
489489

490-
def create_streams(self):
490+
def create_streams(self) -> None:
491491
"""Create the streams for all installations."""
492492
for install in self.zaptec.installations:
493493
if install.id in self.zaptec:
@@ -501,7 +501,7 @@ def create_streams(self):
501501
)
502502
self.streams.append((task, install))
503503

504-
async def cancel_streams(self):
504+
async def cancel_streams(self) -> None:
505505
"""Cancel all streams for the account."""
506506
for task, install in self.streams:
507507
_LOGGER.debug("Cancelling stream for %s", install.qual_id)
@@ -707,7 +707,7 @@ async def trigger_poll(self) -> None:
707707
finally:
708708
self._trigger_task = None
709709

710-
def cleanup_task(_task: asyncio.Task):
710+
def cleanup_task(_task: asyncio.Task) -> None:
711711
"""Cleanup the task after it has run."""
712712
self._trigger_task = None
713713

@@ -797,6 +797,7 @@ def _get_zaptec_value(
797797
Raises:
798798
KeyUnavailableError: If key doesn't exist or obj doesn't have
799799
`.get()`, which indicates that obj isn't a Mapping-like object
800+
800801
"""
801802
obj = self.zaptec_obj
802803
key = key or self.key
@@ -835,7 +836,7 @@ def _log_zaptec_attribute(self) -> str:
835836
return f".{v}"
836837

837838
@callback
838-
def _log_value(self, attribute: str | None, force=False):
839+
def _log_value(self, attribute: str | None, force=False) -> None:
839840
"""Helper to log a new value."""
840841
if attribute is None:
841842
return
@@ -854,7 +855,7 @@ def _log_value(self, attribute: str | None, force=False):
854855
)
855856

856857
@callback
857-
def _log_unavailable(self, exception: Exception | None = None):
858+
def _log_unavailable(self, exception: Exception | None = None) -> None:
858859
"""Helper to log when unavailable."""
859860
available = self._attr_available
860861
prev_available = self._prev_available
@@ -885,7 +886,7 @@ def _log_unavailable(self, exception: Exception | None = None):
885886
_LOGGER.info("Entity %s is available", self.entity_id)
886887

887888
@property
888-
def key(self):
889+
def key(self) -> str:
889890
"""Helper to retrieve the key from the entity description."""
890891
return self.entity_description.key
891892

custom_components/zaptec/api.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,7 +1104,7 @@ async def _refresh_token(self):
11041104
_LOGGER.debug(" TOKEN OK")
11051105
return
11061106

1107-
elif response.status == 400:
1107+
if response.status == 400:
11081108
data = await response.json()
11091109
raise log_exc(
11101110
AuthenticationError(
@@ -1153,11 +1153,11 @@ async def request(self, url: str, *, method="get", data=None, base_url=API_URL):
11531153
kwargs["headers"]["Authorization"] = f"Bearer {self._access_token}"
11541154
continue # Retry request
11551155

1156-
elif response.status in (201, 204): # Created, no content
1156+
if response.status in (201, 204): # Created, no content
11571157
content = await response.read()
11581158
return content
11591159

1160-
elif response.status == 200: # OK
1160+
if response.status == 200: # OK
11611161
# Read the JSON payload
11621162
try:
11631163
json_result = await response.json(content_type=None)
@@ -1300,7 +1300,7 @@ async def build(self):
13001300

13011301
# Update the observation, settings and commands ids based on the
13021302
# discovered device types.
1303-
ZCONST.update_ids_from_schema({chg["DeviceType"] for chg in self.chargers})
1303+
ZCONST.update_ids_from_schema({str(chg["DeviceType"]) for chg in self.chargers})
13041304

13051305
self.is_built = True
13061306

custom_components/zaptec/binary_sensor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def _update_from_zaptec(self) -> None:
3737
class ZaptecBinarySensorWithAttrs(ZaptecBinarySensor):
3838
"""Zaptec binary sensor with additional attributes."""
3939

40-
def _post_init(self):
40+
def _post_init(self) -> None:
4141
self._attr_extra_state_attributes = self.zaptec_obj.asdict()
4242
self._attr_unique_id = self.zaptec_obj.id
4343

custom_components/zaptec/config_flow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ async def _get_chargers(self) -> tuple[dict[str, str], dict[str, str]]:
8888
_LOGGER.exception("Unexpected exception")
8989
errors["base"] = "unknown"
9090

91-
def charger_text(charger: Charger):
91+
def charger_text(charger: Charger) -> str:
9292
"""Format the charger text for display."""
9393
text = f"{charger.name} ({charger.get('DeviceId', '-')})"
9494
if circuit := charger.get("CircuitName"):

custom_components/zaptec/sensor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class ZaptecChargeSensor(ZaptecSensorTranslate):
8888
def _update_from_zaptec(self) -> None:
8989
"""Update the entity from Zaptec data."""
9090
# Called from ZaptecBaseEntity._handle_coordinator_update()
91-
self._attr_native_value = self._get_zaptec_value(lower_case_str=True)
91+
self._attr_native_value: str = self._get_zaptec_value(lower_case_str=True)
9292
self._attr_icon = self.CHARGE_MODE_ICON_MAP.get(
9393
self._attr_native_value, self.CHARGE_MODE_ICON_MAP["unknown"]
9494
)

custom_components/zaptec/zconst.py

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from collections import UserDict
66
import json
77
import logging
8-
from typing import ClassVar, Literal
8+
from typing import Any, ClassVar, Literal
99

1010
from .misc import to_under
1111

@@ -26,7 +26,7 @@
2626
# Helper wrapper for reading constants from the API
2727
#
2828
class ZConst(UserDict):
29-
"""Zaptec constants wrapper class"""
29+
"""Zaptec constants wrapper class."""
3030

3131
observations: dict[str, int]
3232
settings: dict[str, int]
@@ -53,7 +53,9 @@ class ZConst(UserDict):
5353
}
5454
"""Mapping of charger models to device serial number prefixes."""
5555

56-
def get_remap(self, wanted, device_types=None) -> dict:
56+
def get_remap(
57+
self, wanted: list[str], device_types: set[str] | None = None
58+
) -> dict[str, int]:
5759
"""Parse the Zaptec constants and return a remap dict.
5860
5961
Parse the given zaptec constants record `CONST` and generate
@@ -83,7 +85,7 @@ def get_remap(self, wanted, device_types=None) -> dict:
8385
ids.update({v: k for k, v in ids.items()})
8486
return ids
8587

86-
def update_ids_from_schema(self, device_types):
88+
def update_ids_from_schema(self, device_types: set[str] | None) -> None:
8789
"""Update the id from a schema.
8890
8991
Read observations, settings and command ids from the
@@ -105,27 +107,27 @@ def update_ids_from_schema(self, device_types):
105107
# DATA EXTRACTION
106108
#
107109
@property
108-
def charger_operation_modes_list(self):
110+
def charger_operation_modes_list(self) -> list[str]:
109111
"""Return a list of all charger operation modes."""
110112
return list(self.get("ChargerOperationModes", {}))
111113

112114
@property
113-
def device_types_list(self):
115+
def device_types_list(self) -> list[str]:
114116
"""Return a list of all device types."""
115117
return list(self.get("DeviceTypes", {}))
116118

117119
@property
118-
def installation_authentication_type_list(self):
120+
def installation_authentication_type_list(self) -> list[str]:
119121
"""Return a list of all installation authentication types."""
120122
return list(self.get("InstallationAuthenticationType", {}))
121123

122124
@property
123-
def installation_types_list(self):
125+
def installation_types_list(self) -> list[str]:
124126
"""Return a list of all installation types."""
125127
return list(self.get("InstallationTypes", {}))
126128

127129
@property
128-
def network_types_list(self):
130+
def network_types_list(self) -> list[str]:
129131
"""Return a list of all electrical network types."""
130132
return list(self.get("NetworkTypes", {}))
131133

@@ -143,53 +145,52 @@ def serial_to_model(self) -> dict[str, str]:
143145
# Want these to be methods so they can be used for type convertions of
144146
# attributes in the API responses.
145147
#
146-
def type_authentication_type(self, v):
148+
def type_authentication_type(self, val: int) -> str:
147149
"""Convert the authentication type to a string."""
148150
modes = {str(v): k for k, v in self.get("InstallationAuthenticationType", {}).items()}
149-
return modes.get(str(v), str(v))
151+
return modes.get(str(val), str(val))
150152

151-
def type_completed_session(self, data):
153+
def type_completed_session(self, val: str) -> dict[str, Any]:
152154
"""Convert the CompletedSession to a dict."""
153-
data = json.loads(data)
155+
data = json.loads(val)
154156
if "SignedSession" in data:
155157
data["SignedSession"] = self.type_ocmf(data["SignedSession"])
156158
return data
157159

158-
def type_device_type(self, v):
160+
def type_device_type(self, val: int) -> str:
159161
"""Convert the device type to a string."""
160162
modes = {str(v): k for k, v in self.get("DeviceTypes", {}).items()}
161-
return modes.get(str(v), str(v))
163+
return modes.get(str(val), str(val))
162164

163-
def type_installation_type(self, v):
165+
def type_installation_type(self, val: int) -> str:
164166
"""Convert the installation type to a string."""
165167
modes = {
166168
str(v.get("Id")): v.get("Name") for v in self.get("InstallationTypes", {}).values()
167169
}
168-
return modes.get(str(v), str(v))
170+
return modes.get(str(val), str(val))
169171

170-
def type_network_type(self, v):
172+
def type_network_type(self, val: int) -> str:
171173
"""Convert the network type to a string."""
172174
modes = {str(v): k for k, v in self.get("NetworkTypes", {}).items()}
173-
return modes.get(str(v), str(v))
175+
return modes.get(str(val), str(val))
174176

175-
def type_ocmf(self, data):
177+
def type_ocmf(self, data: str) -> dict[str, Any]:
176178
"""Open Charge Metering Format (OCMF) type."""
177179
# https://github.com/SAFE-eV/OCMF-Open-Charge-Metering-Format/blob/master/OCMF-en.md
178180
sects = data.split("|")
179181
if len(sects) not in (2, 3) or sects[0] != "OCMF":
180182
raise ValueError(f"Invalid OCMF data: {data}")
181-
data = json.loads(sects[1])
182-
return data
183+
return json.loads(sects[1])
183184

184-
def type_charger_operation_mode(self, v):
185+
def type_charger_operation_mode(self, val: int) -> str:
185186
"""Convert the operation mode to a string."""
186187
modes = {str(v): k for k, v in self.get("ChargerOperationModes", {}).items()}
187-
return modes.get(str(v), str(v))
188+
return modes.get(str(val), str(val))
188189

189-
def type_user_roles(self, v):
190+
def type_user_roles(self, val: int) -> str:
190191
"""Convert the user roles to a string."""
191-
val = int(v)
192-
if not val:
192+
if val == 0:
193193
return "None"
194-
roles = set(k for k, v in self.get("UserRoles", {}).items() if v & val == v)
194+
# v != 0 is needed to avoid the 0 == 0 case leading to None always being included
195+
roles = {k for k, v in self.get("UserRoles", {}).items() if v != 0 and (v & val) == v}
195196
return ", ".join(roles)

0 commit comments

Comments
 (0)