8000 Enable strict typing for zeroconf (#48450) · home-assistant/core@82c9482 · GitHub
[go: up one dir, main page]

Skip to content

Commit 82c9482

Browse f 8000 iles
authored
Enable strict typing for zeroconf (#48450)
* Enable strict typing for zeroconf * Fix lutron_caseta * Fix pylint warning * Fix tests * Fix xiaomi_aqara test * Add __init__.py in homeassistant.generated module * Restore add_job with type: ignore
1 parent 338be8c commit 82c9482

File tree

9 files changed

+111
-92
lines changed

9 files changed

+111
-92
lines changed

homeassistant/components/lutron_caseta/config_flow.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import voluptuous as vol
1111

1212
from homeassistant import config_entries
13-
from homeassistant.components.zeroconf import ATTR_HOSTNAME
1413
from homeassistant.const import CONF_HOST, CONF_NAME
1514
from homeassistant.core import callback
1615

@@ -66,7 +65,7 @@ async def async_step_user(self, user_input=None):
6665

6766
async def async_step_zeroconf(self, discovery_info):
6867
"""Handle a flow initialized by zeroconf discovery."""
69-
hostname = discovery_info[ATTR_HOSTNAME]
68+
hostname = discovery_info["hostname"]
7069
if hostname is None or not hostname.startswith("lutron-"):
7170
return self.async_abort(reason="not_lutron_device")
7271

homeassistant/components/zeroconf/__init__.py

Lines changed: 55 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -7,46 +7,39 @@
77
import ipaddress
88
import logging
99
import socket
10+
from typing import Any, TypedDict
1011

1112
import voluptuous as vol
1213
from zeroconf import (
13-
DNSPointer,
14-
DNSRecord,
1514
Error as ZeroconfError,
1615
InterfaceChoice,
1716
IPVersion,
1817
NonUniqueNameException,
19-
ServiceBrowser,
2018
ServiceInfo,
2119
ServiceStateChange,
2220
Zeroconf,
2321
)
2422

2523
from homeassistant import util
2624
from homeassistant.const import (
27-
ATTR_NAME,
2825
EVENT_HOMEASSISTANT_START,
2926
EVENT_HOMEASSISTANT_STARTED,
3027
EVENT_HOMEASSISTANT_STOP,
3128
__version__,
3229
)
30+
from homeassistant.core import Event, HomeAssistant
3331
import homeassistant.helpers.config_validation as cv
3432
from homeassistant.helpers.network import NoURLAvailableError, get_url
3533
from homeassistant.helpers.singleton import singleton
3634
from homeassistant.loader import async_get_homekit, async_get_zeroconf
3735

36+
from .models import HaServiceBrowser, HaZeroconf
3837
from .usage import install_multiple_zeroconf_catcher
3938

4039
_LOGGER = logging.getLogger(__name__)
4140

4241
DOMAIN = "zeroconf"
4342

44-
ATTR_HOST = "host"
45-
ATTR_PORT = "port"
46-
ATTR_HOSTNAME = "hostname"
47-
ATTR_TYPE = "type"
48-
ATTR_PROPERTIES = "properties"
49-
5043
ZEROCONF_TYPE = "_home-assistant._tcp.local."
5144
HOMEKIT_TYPES = [
5245
"_hap._tcp.local.",
@@ -59,7 +52,6 @@
5952
DEFAULT_DEFAULT_INTERFACE = True
6053
DEFAULT_IPV6 = True
6154

62-
HOMEKIT_PROPERTIES = "properties"
6355
HOMEKIT_PAIRED_STATUS_FLAG = "sf"
6456
HOMEKIT_MODEL = "md"
6557

@@ -85,20 +77,31 @@
8577
)
8678

8779

80+
class HaServiceInfo(TypedDict):
81+
"""Prepared info from mDNS entries."""
82+
83+
host: str
84+
port: int | None
85+
hostname: str
86+
type: str
87+
name: str
88+
properties: dict[str, Any]
89+
90+
8891
@singleton(DOMAIN)
89-
async def async_get_instance(hass):
92+
async def async_get_instance(hass: HomeAssistant) -> HaZeroconf:
9093
"""Zeroconf instance to be shared with other integrations that use it."""
9194
return await _async_get_instance(hass)
9295

9396

94-
async def _async_get_instance(hass, **zcargs):
97+
async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaZeroconf:
9598
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
9699

97100
zeroconf = await hass.async_add_executor_job(partial(HaZeroconf, **zcargs))
98101

99102
install_multiple_zeroconf_catcher(zeroconf)
100103

101-
def _stop_zeroconf(_):
104+
def _stop_zeroconf(_event: Event) -> None:
102105
"""Stop Zeroconf."""
103106
zeroconf.ha_close()
104107

@@ -107,48 +110,18 @@ def _stop_zeroconf(_):
107110
return zeroconf
108111

109112

110-
class HaServiceBrowser(ServiceBrowser):
111-
"""ServiceBrowser that only consumes DNSPointer records."""
112-
113-
def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
114-
"""Pre-Filter update_record to DNSPointers for the configured type."""
115-
116-
#
117-
# Each ServerBrowser currently runs in its own thread which
118-
# processes every A or AAAA record update per instance.
119-
#
120-
# As the list of zeroconf names we watch for grows, each additional
121-
# ServiceBrowser would process all the A and AAAA updates on the network.
122-
#
123-
# To avoid overwhemling the system we pre-filter here and only process
124-
# DNSPointers for the configured record name (type)
125-
#
126-
if record.name not in self.types or not isinstance(record, DNSPointer):
127-
return
128-
super().update_record(zc, now, record)
129-
130-
131-
class HaZeroconf(Zeroconf):
132-
"""Zeroconf that cannot be closed."""
133-
134-
def close(self):
135-
"""Fake method to avoid integrations closing it."""
136-
137-
ha_close = Zeroconf.close
138-
139-
140-
async def async_setup(hass, config):
113+
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
141114
"""Set up Zeroconf and make Home Assistant discoverable."""
142115
zc_config = config.get(DOMAIN, {})
143-
zc_args = {}
116+
zc_args: dict = {}
144117
if zc_config.get(CONF_DEFAULT_INTERFACE, DEFAULT_DEFAULT_INTERFACE):
145118
zc_args["interfaces"] = InterfaceChoice.Default
146119
if not zc_config.get(CONF_IPV6, DEFAULT_IPV6):
147120
zc_args["ip_version"] = IPVersion.V4Only
148121

149122
zeroconf = hass.data[DOMAIN] = await _async_get_instance(hass, **zc_args)
150123

151-
async def _async_zeroconf_hass_start(_event):
124+
async def _async_zeroconf_hass_start(_event: Event) -> None:
152125
"""Expose Home Assistant on zeroconf when it starts.
153126
154127
Wait till started or otherwise HTTP is not up and running.
@@ -158,7 +131,7 @@ async def _async_zeroconf_hass_start(_event):
158131
_register_hass_zc_service, hass, zeroconf, uuid
159132
)
160133

161-
async def _async_zeroconf_hass_started(_event):
134+
async def _async_zeroconf_hass_started(_event: Event) -> None:
162135
"""Start the service browser."""
163136

164137
await _async_start_zeroconf_browser(hass, zeroconf)
@@ -171,7 +144,9 @@ async def _async_zeroconf_hass_started(_event):
171144
return True
172145

173146

174-
def _register_hass_zc_service(hass, zeroconf, uuid):
147+
def _register_hass_zc_service(
148+
hass: HomeAssistant, zeroconf: HaZeroconf, uuid: str
149+
) -> None:
175150
# Get instance UUID
176151
valid_location_name = _truncate_location_name_to_valid(hass.config.location_name)
177152

@@ -224,7 +199,9 @@ def _register_hass_zc_service(hass, zeroconf, uuid):
224199
)
225200

226201

227-
async def _async_start_zeroconf_browser(hass, zeroconf):
202+
async def _async_start_zeroconf_browser(
203+
hass: HomeAssistant, zeroconf: HaZeroconf
204+
) -> None:
228205
"""Start the zeroconf browser."""
229206

230207
zeroconf_types = await async_get_zeroconf(hass)
@@ -236,7 +213,12 @@ async def _async_start_zeroconf_browser(hass, zeroconf):
236213
if hk_type not in zeroconf_types:
237214
types.append(hk_type)
238215

239-
def service_update(zeroconf, service_type, name, state_change):
216+
def service_update(
217+
zeroconf: Zeroconf,
218+
service_type: str,
219+
name: str,
220+
state_change: ServiceStateChange,
221+
) -> None:
240222
"""Service state changed."""
241223
nonlocal zeroconf_types
242224
nonlocal homekit_models
@@ -276,25 +258,24 @@ def service_update(zeroconf, service_type, name, state_change):
276258
# offering a second discovery for the same device
277259
if (
278260
discovery_was_forwarded
279-
and HOMEKIT_PROPERTIES in info
280-
and HOMEKIT_PAIRED_STATUS_FLAG in info[HOMEKIT_PROPERTIES]
261+
and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"]
281262
):
282263
try:
283264
# 0 means paired and not discoverable by iOS clients)
284-
if int(info[HOMEKIT_PROPERTIES][HOMEKIT_PAIRED_STATUS_FLAG]):
265+
if int(info["properties"][HOMEKIT_PAIRED_STATUS_FLAG]):
285266
return
286267
except ValueError:
287268
# HomeKit pairing status unknown
288269
# likely bad homekit data
289270
return
290271

291272
if "name" in info:
292-
lowercase_name = info["name"].lower()
273+
lowercase_name: str | None = info["name"].lower()
293274
else:
294275
lowercase_name = None
295276

296-
if "macaddress" in info.get("properties", {}):
297-
uppercase_mac = info["properties"]["macaddress"].upper()
277+
if "macaddress" in info["properties"]:
278+
uppercase_mac: str | None = info["properties"]["macaddress"].upper()
298279
else:
299280
uppercase_mac = None
300281

@@ -318,20 +299,22 @@ def service_update(zeroconf, service_type, name, state_change):
318299
hass.add_job(
319300
hass.config_entries.flow.async_init(
320301
entry["domain"], context={"source": DOMAIN}, data=info
321-
)
302+
) # type: ignore
322303
)
323304

324305
_LOGGER.debug("Starting Zeroconf browser")
325306
HaServiceBrowser(zeroconf, types, handlers=[service_update])
326307

327308

328-
def handle_homekit(hass, homekit_models, info) -> bool:
309+
def handle_homekit(
310+
hass: HomeAssistant, homekit_models: dict[str, str], info: HaServiceInfo
311+
) -> bool:
329312
"""Handle a HomeKit discovery.
330313
331314
Return if discovery was forwarded.
332315
"""
333316
model = None
334-
props = info.get(HOMEKIT_PROPERTIES, {})
317+
props = info["properties"]
335318

336319
for key in props:
337320
if key.lower() == HOMEKIT_MODEL:
@@ -352,16 +335,16 @@ def handle_homekit(hass, homekit_models, info) -> bool:
352335
hass.add_job(
353336
hass.config_entries.flow.async_init(
354337
homekit_models[test_model], context={"source": "homekit"}, data=info
355-
)
338+
) # type: ignore
356339
)
357340
return True
358341

359342
return False
360343

361344

362-
def info_from_service(service):
345+
def info_from_service(service: ServiceInfo) -> HaServiceInfo | None:
363346
"""Return prepared info from mDNS entries."""
364-
properties = {"_raw": {}}
347+
properties: dict[str, Any] = {"_raw": {}}
365348

366349
for key, value in service.properties.items():
367350
# See https://ietf.org/rfc/rfc6763.html#section-6.4 and
@@ -386,19 +369,17 @@ def info_from_service(service):
386369

387370
address = service.addresses[0]
388371

389-
info = {
390-
ATTR_HOST: str(ipaddress.ip_address(address)),
391-
ATTR_PORT: service.port,
392-
ATTR_HOSTNAME: service.server,
393-
ATTR_TYPE: service.type,
394-
ATTR_NAME: service.name,
395-
ATTR_PROPERTIES: properties,
372+
return {
373+
"host": str(ipaddress.ip_address(address)),
374+
"port": service.port,
375+
"hostname": service.server,
376+
"type": service.type,
377+
"name": service.name,
378+
"properties": properties,
396379
}
397380

398-
return info
399-
400381

401-
def _suppress_invalid_properties(properties):
382+
def _suppress_invalid_properties(properties: dict) -> None:
402383
"""Suppress any properties that will cause zeroconf to fail to startup."""
403384

404385
for prop, prop_value in properties.items():
@@ -415,7 +396,7 @@ def _suppress_invalid_properties(properties):
415396
properties[prop] = ""
416397

417398

418-
def _truncate_location_name_to_valid(location_name):
399+
def _truncate_location_name_to_valid(location_name: str) -> str:
419400
"""Truncate or return the location name usable for zeroconf."""
420401
if len(location_name.encode("utf-8")) < MAX_NAME_LEN:
421402
return location_name
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Models for Zeroconf."""
2+
3+
from zeroconf import DNSPointer, DNSRecord, ServiceBrowser, Zeroconf
4+
5+
6+
class HaZeroconf(Zeroconf):
7+
"""Zeroconf that cannot be closed."""
8+
9+
def close(self) -> None:
10+
"""Fake method to avoid integrations closing it."""
11+
12+
ha_close = Zeroconf.close
13+
14+
15+
class HaServiceBrowser(ServiceBrowser):
16+
"""ServiceBrowser that only consumes DNSPointer records."""
17+
18+
def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
19+
"""Pre-Filter update_record to DNSPointers for the configured type."""
20+
21+
#
22+
# Each ServerBrowser currently runs in its own thread which
23+
# processes every A or AAAA record update per instance.
24+
#
25+
# As the list of zeroconf names we watch for grows, each additional
26+
# ServiceBrowser would process all the A and AAAA updates on the network.
27+
#
28+
# To avoid overwhemling the system we pre-filter here and only process
29+
# DNSPointers for the configured record name (type)
30+
#
31+
if record.name not in self.types or not isinstance(record, DNSPointer):
32+
return
33+
super().update_record(zc, now, record)

homeassistant/components/zeroconf/usage.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from contextlib import suppress
44
import logging
5+
from typing import Any
56

67
import zeroconf
78

@@ -11,23 +12,25 @@
1112
report_integration,
1213
)
1314

15+
from .models import HaZeroconf
16+
1417
_LOGGER = logging.getLogger(__name__)
1518

1619

17-
def install_multiple_zeroconf_catcher(hass_zc) -> None:
20+
def install_multiple_zeroconf_catcher(hass_zc: HaZeroconf) -> None:
1821
"""Wrap the Zeroconf class to return the shared instance if multiple instances are detected."""
1922

20-
def new_zeroconf_new(self, *k, **kw):
23+
def new_zeroconf_new(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> HaZeroconf:
2124
_report(
2225
"attempted to create another Zeroconf instance. Please use the shared Zeroconf via await homeassistant.components.zeroconf.async_get_instance(hass)",
2326
)
2427
return hass_zc
2528

26-
def new_zeroconf_init(self, *k, **kw):
29+
def new_zeroconf_init(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> None:
2730
return
2831

29-
zeroconf.Zeroconf.__new__ = new_zeroconf_new
30-
zeroconf.Zeroconf.__init__ = new_zeroconf_init
32+
zeroconf.Zeroconf.__new__ = new_zeroconf_new # type: ignore
33+
zeroconf.Zeroconf.__init__ = new_zeroconf_init # type: ignore
3134

3235

3336
def _report(what: str) -> None:

homeassistant/generated/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""All files in this module are automatically generated by hassfest.
2+
3+
To update, run python3 -m script.hassfest
4+
"""

0 commit comments

Comments
 (0)
0