7
7
import ipaddress
8
8
import logging
9
9
import socket
10
+ from typing import Any , TypedDict
10
11
11
12
import voluptuous as vol
12
13
from zeroconf import (
13
- DNSPointer ,
14
- DNSRecord ,
15
14
Error as ZeroconfError ,
16
15
InterfaceChoice ,
17
16
IPVersion ,
18
17
NonUniqueNameException ,
19
- ServiceBrowser ,
20
18
ServiceInfo ,
21
19
ServiceStateChange ,
22
20
Zeroconf ,
23
21
)
24
22
25
23
from homeassistant import util
26
24
from homeassistant .const import (
27
- ATTR_NAME ,
28
25
EVENT_HOMEASSISTANT_START ,
29
26
EVENT_HOMEASSISTANT_STARTED ,
30
27
EVENT_HOMEASSISTANT_STOP ,
31
28
__version__ ,
32
29
)
30
+ from homeassistant .core import Event , HomeAssistant
33
31
import homeassistant .helpers .config_validation as cv
34
32
from homeassistant .helpers .network import NoURLAvailableError , get_url
35
33
from homeassistant .helpers .singleton import singleton
36
34
from homeassistant .loader import async_get_homekit , async_get_zeroconf
37
35
36
+ from .models import HaServiceBrowser , HaZeroconf
38
37
from .usage import install_multiple_zeroconf_catcher
39
38
40
39
_LOGGER = logging .getLogger (__name__ )
41
40
42
41
DOMAIN = "zeroconf"
43
42
44
- ATTR_HOST = "host"
45
- ATTR_PORT = "port"
46
- ATTR_HOSTNAME = "hostname"
47
- ATTR_TYPE = "type"
48
- ATTR_PROPERTIES = "properties"
49
-
50
43
ZEROCONF_TYPE = "_home-assistant._tcp.local."
51
44
HOMEKIT_TYPES = [
52
45
"_hap._tcp.local." ,
59
52
DEFAULT_DEFAULT_INTERFACE = True
60
53
DEFAULT_IPV6 = True
61
54
62
- HOMEKIT_PROPERTIES = "properties"
63
55
HOMEKIT_PAIRED_STATUS_FLAG = "sf"
64
56
HOMEKIT_MODEL = "md"
65
57
85
77
)
86
78
87
79
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
+
88
91
@singleton (DOMAIN )
89
- async def async_get_instance (hass ) :
92
+ async def async_get_instance (hass : HomeAssistant ) -> HaZeroconf :
90
93
"""Zeroconf instance to be shared with other integrations that use it."""
91
94
return await _async_get_instance (hass )
92
95
93
96
94
- async def _async_get_instance (hass , ** zcargs ) :
97
+ async def _async_get_instance (hass : HomeAssistant , ** zcargs : Any ) -> HaZeroconf :
95
98
logging .getLogger ("zeroconf" ).setLevel (logging .NOTSET )
96
99
97
100
zeroconf = await hass .async_add_executor_job (partial (HaZeroconf , ** zcargs ))
98
101
99
102
install_multiple_zeroconf_catcher (zeroconf )
100
103
101
- def _stop_zeroconf (_ ) :
104
+ def _stop_zeroconf (_event : Event ) -> None :
102
105
"""Stop Zeroconf."""
103
106
zeroconf .ha_close ()
104
107
@@ -107,48 +110,18 @@ def _stop_zeroconf(_):
107
110
return zeroconf
108
111
109
112
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 :
141
114
"""Set up Zeroconf and make Home Assistant discoverable."""
142
115
zc_config = config .get (DOMAIN , {})
143
- zc_args = {}
116
+ zc_args : dict = {}
144
117
if zc_config .get (CONF_DEFAULT_INTERFACE , DEFAULT_DEFAULT_INTERFACE ):
145
118
zc_args ["interfaces" ] = InterfaceChoice .Default
146
119
if not zc_config .get (CONF_IPV6 , DEFAULT_IPV6 ):
147
120
zc_args ["ip_version" ] = IPVersion .V4Only
148
121
149
122
zeroconf = hass .data [DOMAIN ] = await _async_get_instance (hass , ** zc_args )
150
123
151
- async def _async_zeroconf_hass_start (_event ) :
124
+ async def _async_zeroconf_hass_start (_event : Event ) -> None :
152
125
"""Expose Home Assistant on zeroconf when it starts.
153
126
154
127
Wait till started or otherwise HTTP is not up and running.
@@ -158,7 +131,7 @@ async def _async_zeroconf_hass_start(_event):
158
131
_register_hass_zc_service , hass , zeroconf , uuid
159
132
)
160
133
161
- async def _async_zeroconf_hass_started (_event ) :
134
+ async def _async_zeroconf_hass_started (_event : Event ) -> None :
162
135
"""Start the service browser."""
163
136
164
137
await _async_start_zeroconf_browser (hass , zeroconf )
@@ -171,7 +144,9 @@ async def _async_zeroconf_hass_started(_event):
171
144
return True
172
145
173
146
174
- def _register_hass_zc_service (hass , zeroconf , uuid ):
147
+ def _register_hass_zc_service (
148
+ hass : HomeAssistant , zeroconf : HaZeroconf , uuid : str
149
+ ) -> None :
175
150
# Get instance UUID
176
151
valid_location_name = _truncate_location_name_to_valid (hass .config .location_name )
177
152
@@ -224,7 +199,9 @@ def _register_hass_zc_service(hass, zeroconf, uuid):
224
199
)
225
200
226
201
227
- async def _async_start_zeroconf_browser (hass , zeroconf ):
202
+ async def _async_start_zeroconf_browser (
203
+ hass : HomeAssistant , zeroconf : HaZeroconf
204
+ ) -> None :
228
205
"""Start the zeroconf browser."""
229
206
230
207
zeroconf_types = await async_get_zeroconf (hass )
@@ -236,7 +213,12 @@ async def _async_start_zeroconf_browser(hass, zeroconf):
236
213
if hk_type not in zeroconf_types :
237
214
types .append (hk_type )
238
215
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 :
240
222
"""Service state changed."""
241
223
nonlocal zeroconf_types
242
224
nonlocal homekit_models
@@ -276,25 +258,24 @@ def service_update(zeroconf, service_type, name, state_change):
276
258
# offering a second discovery for the same device
277
259
if (
278
260
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" ]
281
262
):
282
263
try :
283
264
# 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 ]):
285
266
return
286
267
except ValueError :
287
268
# HomeKit pairing status unknown
288
269
# likely bad homekit data
289
270
return
290
271
291
272
if "name" in info :
292
- lowercase_name = info ["name" ].lower ()
273
+ lowercase_name : str | None = info ["name" ].lower ()
293
274
else :
294
275
lowercase_name = None
295
276
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 ()
298
279
else :
299
280
uppercase_mac = None
300
281
@@ -318,20 +299,22 @@ def service_update(zeroconf, service_type, name, state_change):
318
299
hass .add_job (
319
300
hass .config_entries .flow .async_init (
320
301
entry ["domain" ], context = {"source" : DOMAIN }, data = info
321
- )
302
+ ) # type: ignore
322
303
)
323
304
324
305
_LOGGER .debug ("Starting Zeroconf browser" )
325
306
HaServiceBrowser (zeroconf , types , handlers = [service_update ])
326
307
327
308
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 :
329
312
"""Handle a HomeKit discovery.
330
313
331
314
Return if discovery was forwarded.
332
315
"""
333
316
model = None
334
- props = info . get ( HOMEKIT_PROPERTIES , {})
317
+ props = info [ "properties" ]
335
318
336
319
for key in props :
337
320
if key .lower () == HOMEKIT_MODEL :
@@ -352,16 +335,16 @@ def handle_homekit(hass, homekit_models, info) -> bool:
352
335
hass .add_job (
353
336
hass .config_entries .flow .async_init (
354
337
homekit_models [test_model ], context = {"source" : "homekit" }, data = info
355
- )
338
+ ) # type: ignore
356
339
)
357
340
return True
358
341
359
342
return False
360
343
361
344
362
- def info_from_service (service ) :
345
+ def info_from_service (service : ServiceInfo ) -> HaServiceInfo | None :
363
346
"""Return prepared info from mDNS entries."""
364
- properties = {"_raw" : {}}
347
+ properties : dict [ str , Any ] = {"_raw" : {}}
365
348
366
349
for key , value in service .properties .items ():
367
350
# See https://ietf.org/rfc/rfc6763.html#section-6.4 and
@@ -386,19 +369,17 @@ def info_from_service(service):
386
369
387
370
address = service .addresses [0 ]
388
371
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 ,
396
379
}
397
380
398
- return info
399
-
400
381
401
- def _suppress_invalid_properties (properties ) :
382
+ def _suppress_invalid_properties (properties : dict ) -> None :
402
383
"""Suppress any properties that will cause zeroconf to fail to startup."""
403
384
404
385
for prop , prop_value in properties .items ():
@@ -415,7 +396,7 @@ def _suppress_invalid_properties(properties):
415
396
properties [prop ] = ""
416
397
417
398
418
- def _truncate_location_name_to_valid (location_name ) :
399
+ def _truncate_location_name_to_valid (location_name : str ) -> str :
419
400
"""Truncate or return the location name usable for zeroconf."""
420
401
if len (location_name .encode ("utf-8" )) < MAX_NAME_LEN :
421
402
return location_name
0 commit comments