steamloop 1.1.0__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {steamloop-1.1.0 → steamloop-1.2.0}/PKG-INFO +8 -2
- {steamloop-1.1.0 → steamloop-1.2.0}/README.md +7 -1
- {steamloop-1.1.0 → steamloop-1.2.0}/pyproject.toml +1 -1
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop/__init__.py +1 -1
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop/cli.py +23 -12
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop/connection.py +20 -3
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop/const.py +1 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop.egg-info/PKG-INFO +8 -2
- {steamloop-1.1.0 → steamloop-1.2.0}/tests/test_cli.py +65 -3
- {steamloop-1.1.0 → steamloop-1.2.0}/tests/test_connection.py +37 -2
- {steamloop-1.1.0 → steamloop-1.2.0}/LICENSE +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/setup.cfg +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop/__main__.py +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop/certs.py +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop/exceptions.py +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop/models.py +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop/py.typed +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop.egg-info/SOURCES.txt +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop.egg-info/dependency_links.txt +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop.egg-info/entry_points.txt +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop.egg-info/requires.txt +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/src/steamloop.egg-info/top_level.txt +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/tests/test_certs.py +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/tests/test_const.py +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/tests/test_dunder_main.py +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/tests/test_exceptions.py +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/tests/test_models.py +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/tests/test_pairing.py +0 -0
- {steamloop-1.1.0 → steamloop-1.2.0}/tests/test_protocol.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: steamloop
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Local control for choochoo based thermostats
|
|
5
5
|
Author-email: "J. Nick Koston" <nick@koston.org>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -68,7 +68,7 @@ pip install steamloop
|
|
|
68
68
|
|
|
69
69
|
### Pairing
|
|
70
70
|
|
|
71
|
-
Put the thermostat in pairing mode (Menu > Settings > Remote
|
|
71
|
+
Put the thermostat in pairing mode (Menu > Settings > Network > Advanced Setup > Remote Connection > Pair), then:
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
74
|
steamloop 192.168.1.100 --pair
|
|
@@ -82,6 +82,12 @@ This saves a pairing file in the current directory with the secret key.
|
|
|
82
82
|
steamloop 192.168.1.100
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
If already paired, you can pass the secret key directly to skip the pairing file:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
steamloop 192.168.1.100 --key YOUR_SECRET_KEY
|
|
89
|
+
```
|
|
90
|
+
|
|
85
91
|
Interactive commands: `status`, `heat <temp>`, `cool <temp>`, `mode <off|auto|cool|heat>`, `fan <auto|on|circulate>`, `eheat <on|off>`, `help`.
|
|
86
92
|
|
|
87
93
|
## Library Usage
|
|
@@ -44,7 +44,7 @@ pip install steamloop
|
|
|
44
44
|
|
|
45
45
|
### Pairing
|
|
46
46
|
|
|
47
|
-
Put the thermostat in pairing mode (Menu > Settings > Remote
|
|
47
|
+
Put the thermostat in pairing mode (Menu > Settings > Network > Advanced Setup > Remote Connection > Pair), then:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
50
|
steamloop 192.168.1.100 --pair
|
|
@@ -58,6 +58,12 @@ This saves a pairing file in the current directory with the secret key.
|
|
|
58
58
|
steamloop 192.168.1.100
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
If already paired, you can pass the secret key directly to skip the pairing file:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
steamloop 192.168.1.100 --key YOUR_SECRET_KEY
|
|
65
|
+
```
|
|
66
|
+
|
|
61
67
|
Interactive commands: `status`, `heat <temp>`, `cool <temp>`, `mode <off|auto|cool|heat>`, `fan <auto|on|circulate>`, `eheat <on|off>`, `help`.
|
|
62
68
|
|
|
63
69
|
## Library Usage
|
|
@@ -225,22 +225,30 @@ async def _do_pair(ip: str, port: int) -> None:
|
|
|
225
225
|
await _do_monitor(ip, port)
|
|
226
226
|
|
|
227
227
|
|
|
228
|
-
async def _do_monitor(ip: str, port: int) -> None:
|
|
228
|
+
async def _do_monitor(ip: str, port: int, secret_key: str | None = None) -> None:
|
|
229
229
|
"""Run monitoring mode with interactive command loop."""
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
print("
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
230
|
+
if secret_key is not None:
|
|
231
|
+
print("\n=== MONITORING MODE ===")
|
|
232
|
+
print(f"Using provided secret key for {ip}")
|
|
233
|
+
device_type = "automation"
|
|
234
|
+
device_id = "module"
|
|
235
|
+
else:
|
|
236
|
+
pairing = await load_pairing(ip)
|
|
237
|
+
if not pairing:
|
|
238
|
+
print("No pairing found. Run with --pair or --key first.")
|
|
239
|
+
return
|
|
240
|
+
print("\n=== MONITORING MODE ===")
|
|
241
|
+
print(f"Using saved pairing for {ip}")
|
|
242
|
+
secret_key = pairing["secret_key"]
|
|
243
|
+
device_type = pairing.get("device_type", "automation")
|
|
244
|
+
device_id = pairing.get("device_id", "module")
|
|
237
245
|
|
|
238
246
|
conn = ThermostatConnection(
|
|
239
247
|
ip,
|
|
240
248
|
port,
|
|
241
|
-
secret_key=
|
|
242
|
-
device_type=
|
|
243
|
-
device_id=
|
|
249
|
+
secret_key=secret_key,
|
|
250
|
+
device_type=device_type,
|
|
251
|
+
device_id=device_id,
|
|
244
252
|
)
|
|
245
253
|
|
|
246
254
|
def on_event(msg: dict[str, Any]) -> None:
|
|
@@ -295,6 +303,9 @@ def main() -> None:
|
|
|
295
303
|
help=f"Port (default: {DEFAULT_PORT})",
|
|
296
304
|
)
|
|
297
305
|
parser.add_argument("--pair", action="store_true", help="Enter pairing mode")
|
|
306
|
+
parser.add_argument(
|
|
307
|
+
"--key", help="Secret key from a previous pairing (skip pairing file lookup)"
|
|
308
|
+
)
|
|
298
309
|
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
299
310
|
args = parser.parse_args()
|
|
300
311
|
|
|
@@ -308,4 +319,4 @@ def main() -> None:
|
|
|
308
319
|
if args.pair:
|
|
309
320
|
asyncio.run(_do_pair(args.ip, args.port))
|
|
310
321
|
else:
|
|
311
|
-
asyncio.run(_do_monitor(args.ip, args.port))
|
|
322
|
+
asyncio.run(_do_monitor(args.ip, args.port, secret_key=args.key))
|
|
@@ -23,6 +23,7 @@ from .const import (
|
|
|
23
23
|
CONNECT_TIMEOUT,
|
|
24
24
|
DEFAULT_PORT,
|
|
25
25
|
HEARTBEAT_INTERVAL,
|
|
26
|
+
INITIAL_STATE_TIMEOUT,
|
|
26
27
|
PAIRING_TIMEOUT,
|
|
27
28
|
RECONNECT_DELAY,
|
|
28
29
|
RECONNECT_MAX,
|
|
@@ -486,6 +487,10 @@ class ThermostatConnection:
|
|
|
486
487
|
"""
|
|
487
488
|
Authenticate with the thermostat.
|
|
488
489
|
|
|
490
|
+
After receiving the login response, waits for the initial burst
|
|
491
|
+
of state events (zone discovery, temperatures, etc.) to arrive
|
|
492
|
+
before returning. This ensures ``state`` is fully populated.
|
|
493
|
+
|
|
489
494
|
Returns:
|
|
490
495
|
LoginResponse on success.
|
|
491
496
|
|
|
@@ -507,6 +512,7 @@ class ThermostatConnection:
|
|
|
507
512
|
)
|
|
508
513
|
loop = asyncio.get_running_loop()
|
|
509
514
|
deadline = loop.time() + RESPONSE_TIMEOUT
|
|
515
|
+
login_resp: LoginResponse | None = None
|
|
510
516
|
while (remaining := deadline - loop.time()) > 0:
|
|
511
517
|
try:
|
|
512
518
|
msg = await asyncio.wait_for(queue.get(), timeout=remaining)
|
|
@@ -516,19 +522,30 @@ class ThermostatConnection:
|
|
|
516
522
|
continue
|
|
517
523
|
resp = msg["Response"]
|
|
518
524
|
if "LoginResponse" in resp:
|
|
519
|
-
login_resp
|
|
525
|
+
login_resp = resp["LoginResponse"]
|
|
520
526
|
if login_resp.get("status") == "1":
|
|
521
527
|
_LOGGER.info("Authenticated successfully")
|
|
522
|
-
|
|
528
|
+
break
|
|
523
529
|
raise AuthenticationError(f"Authentication failed: {login_resp}")
|
|
524
530
|
if "Error" in resp:
|
|
525
531
|
err: ErrorResponse = resp["Error"]
|
|
526
532
|
raise AuthenticationError(
|
|
527
533
|
f"Error {err.get('error_type')}: {err.get('description')}"
|
|
528
534
|
)
|
|
535
|
+
if login_resp is None:
|
|
536
|
+
raise AuthenticationError("No login response received")
|
|
537
|
+
# Drain the initial state burst — the thermostat sends zone
|
|
538
|
+
# discovery events right after the login response. Keep
|
|
539
|
+
# reading until the stream goes quiet so that callers see a
|
|
540
|
+
# fully-populated ``state`` when login() returns.
|
|
541
|
+
while True:
|
|
542
|
+
try:
|
|
543
|
+
await asyncio.wait_for(queue.get(), timeout=INITIAL_STATE_TIMEOUT)
|
|
544
|
+
except TimeoutError:
|
|
545
|
+
break
|
|
546
|
+
return login_resp
|
|
529
547
|
finally:
|
|
530
548
|
self._message_queue = None
|
|
531
|
-
raise AuthenticationError("No login response received")
|
|
532
549
|
|
|
533
550
|
async def pair(self) -> SetSecretKeyRequest:
|
|
534
551
|
"""
|
|
@@ -43,6 +43,7 @@ HEARTBEAT_INTERVAL = 55 # seconds (thermostat expects every 60s)
|
|
|
43
43
|
CONNECT_TIMEOUT = 10
|
|
44
44
|
PAIRING_TIMEOUT = 120
|
|
45
45
|
RESPONSE_TIMEOUT = 10
|
|
46
|
+
INITIAL_STATE_TIMEOUT = 1 # seconds to wait for initial state after login
|
|
46
47
|
RECONNECT_DELAY = 5 # initial delay before first reconnect attempt
|
|
47
48
|
RECONNECT_MAX = 300 # max delay between reconnect attempts (5 minutes)
|
|
48
49
|
BACKOFF_FACTOR = 2 # multiply delay by this on each failure
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: steamloop
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Local control for choochoo based thermostats
|
|
5
5
|
Author-email: "J. Nick Koston" <nick@koston.org>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -68,7 +68,7 @@ pip install steamloop
|
|
|
68
68
|
|
|
69
69
|
### Pairing
|
|
70
70
|
|
|
71
|
-
Put the thermostat in pairing mode (Menu > Settings > Remote
|
|
71
|
+
Put the thermostat in pairing mode (Menu > Settings > Network > Advanced Setup > Remote Connection > Pair), then:
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
74
|
steamloop 192.168.1.100 --pair
|
|
@@ -82,6 +82,12 @@ This saves a pairing file in the current directory with the secret key.
|
|
|
82
82
|
steamloop 192.168.1.100
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
If already paired, you can pass the secret key directly to skip the pairing file:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
steamloop 192.168.1.100 --key YOUR_SECRET_KEY
|
|
89
|
+
```
|
|
90
|
+
|
|
85
91
|
Interactive commands: `status`, `heat <temp>`, `cool <temp>`, `mode <off|auto|cool|heat>`, `fan <auto|on|circulate>`, `eheat <on|off>`, `help`.
|
|
86
92
|
|
|
87
93
|
## Library Usage
|
|
@@ -542,7 +542,7 @@ def test_main_pair_mode() -> None:
|
|
|
542
542
|
patch(
|
|
543
543
|
"steamloop.cli.argparse.ArgumentParser.parse_args",
|
|
544
544
|
return_value=MagicMock(
|
|
545
|
-
ip="192.168.1.100", port=7878, pair=True, debug=False
|
|
545
|
+
ip="192.168.1.100", port=7878, pair=True, key=None, debug=False
|
|
546
546
|
),
|
|
547
547
|
),
|
|
548
548
|
patch(
|
|
@@ -561,16 +561,37 @@ def test_main_monitor_mode() -> None:
|
|
|
561
561
|
patch(
|
|
562
562
|
"steamloop.cli.argparse.ArgumentParser.parse_args",
|
|
563
563
|
return_value=MagicMock(
|
|
564
|
-
ip="192.168.1.100", port=7878, pair=False, debug=False
|
|
564
|
+
ip="192.168.1.100", port=7878, pair=False, key=None, debug=False
|
|
565
565
|
),
|
|
566
566
|
),
|
|
567
567
|
patch(
|
|
568
568
|
"steamloop.cli._do_monitor",
|
|
569
569
|
new_callable=lambda: MagicMock(return_value=sentinel),
|
|
570
|
+
) as mock_monitor,
|
|
571
|
+
patch("steamloop.cli.asyncio.run") as mock_run,
|
|
572
|
+
):
|
|
573
|
+
main()
|
|
574
|
+
mock_monitor.assert_called_once_with("192.168.1.100", 7878, secret_key=None)
|
|
575
|
+
mock_run.assert_called_once_with(sentinel)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def test_main_monitor_with_key() -> None:
|
|
579
|
+
sentinel = object()
|
|
580
|
+
with (
|
|
581
|
+
patch(
|
|
582
|
+
"steamloop.cli.argparse.ArgumentParser.parse_args",
|
|
583
|
+
return_value=MagicMock(
|
|
584
|
+
ip="192.168.1.100", port=7878, pair=False, key="my-key", debug=False
|
|
585
|
+
),
|
|
570
586
|
),
|
|
587
|
+
patch(
|
|
588
|
+
"steamloop.cli._do_monitor",
|
|
589
|
+
new_callable=lambda: MagicMock(return_value=sentinel),
|
|
590
|
+
) as mock_monitor,
|
|
571
591
|
patch("steamloop.cli.asyncio.run") as mock_run,
|
|
572
592
|
):
|
|
573
593
|
main()
|
|
594
|
+
mock_monitor.assert_called_once_with("192.168.1.100", 7878, secret_key="my-key")
|
|
574
595
|
mock_run.assert_called_once_with(sentinel)
|
|
575
596
|
|
|
576
597
|
|
|
@@ -579,7 +600,7 @@ def test_main_debug_mode() -> None:
|
|
|
579
600
|
patch(
|
|
580
601
|
"steamloop.cli.argparse.ArgumentParser.parse_args",
|
|
581
602
|
return_value=MagicMock(
|
|
582
|
-
ip="192.168.1.100", port=7878, pair=False, debug=True
|
|
603
|
+
ip="192.168.1.100", port=7878, pair=False, key=None, debug=True
|
|
583
604
|
),
|
|
584
605
|
),
|
|
585
606
|
patch(
|
|
@@ -594,3 +615,44 @@ def test_main_debug_mode() -> None:
|
|
|
594
615
|
|
|
595
616
|
mock_logging.assert_called_once()
|
|
596
617
|
assert mock_logging.call_args[1]["level"] == logging.DEBUG
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
async def test_do_monitor_with_key_skips_pairing_file(
|
|
621
|
+
capsys: pytest.CaptureFixture[str],
|
|
622
|
+
) -> None:
|
|
623
|
+
"""When secret_key is provided, pairing file is not loaded."""
|
|
624
|
+
mock_conn = MagicMock()
|
|
625
|
+
mock_conn.connected = True
|
|
626
|
+
mock_conn.state = ThermostatState()
|
|
627
|
+
mock_conn.add_event_callback = MagicMock(return_value=lambda: None)
|
|
628
|
+
|
|
629
|
+
async def _mock_aenter(self: Any) -> MagicMock:
|
|
630
|
+
return mock_conn
|
|
631
|
+
|
|
632
|
+
mock_conn.__aenter__ = _mock_aenter
|
|
633
|
+
mock_conn.__aexit__ = AsyncMock(return_value=False)
|
|
634
|
+
|
|
635
|
+
with (
|
|
636
|
+
patch(
|
|
637
|
+
"steamloop.cli.load_pairing",
|
|
638
|
+
new_callable=AsyncMock,
|
|
639
|
+
) as mock_load,
|
|
640
|
+
patch(
|
|
641
|
+
"steamloop.cli.ThermostatConnection",
|
|
642
|
+
return_value=mock_conn,
|
|
643
|
+
) as mock_cls,
|
|
644
|
+
patch("steamloop.cli.sys.stdin") as mock_stdin,
|
|
645
|
+
):
|
|
646
|
+
mock_stdin.readline.side_effect = KeyboardInterrupt
|
|
647
|
+
await _do_monitor("192.168.1.100", 7878, secret_key="my-key")
|
|
648
|
+
|
|
649
|
+
mock_load.assert_not_called()
|
|
650
|
+
mock_cls.assert_called_once_with(
|
|
651
|
+
"192.168.1.100",
|
|
652
|
+
7878,
|
|
653
|
+
secret_key="my-key",
|
|
654
|
+
device_type="automation",
|
|
655
|
+
device_id="module",
|
|
656
|
+
)
|
|
657
|
+
captured = capsys.readouterr()
|
|
658
|
+
assert "provided secret key" in captured.out
|
|
@@ -349,7 +349,8 @@ async def test_login_success(connection: ThermostatConnection) -> None:
|
|
|
349
349
|
asyncio.get_event_loop().call_soon(lambda: connection._on_message(resp))
|
|
350
350
|
# Need a small delay for the queue to process
|
|
351
351
|
task = asyncio.create_task(_feed_response(connection, resp))
|
|
352
|
-
|
|
352
|
+
with patch("steamloop.connection.INITIAL_STATE_TIMEOUT", 0.05):
|
|
353
|
+
result = await connection.login()
|
|
353
354
|
assert result["status"] == "1"
|
|
354
355
|
await task
|
|
355
356
|
|
|
@@ -375,6 +376,7 @@ async def test_login_error_response(
|
|
|
375
376
|
async def test_login_timeout(connection: ThermostatConnection) -> None:
|
|
376
377
|
with (
|
|
377
378
|
patch("steamloop.connection.RESPONSE_TIMEOUT", 0.05),
|
|
379
|
+
patch("steamloop.connection.INITIAL_STATE_TIMEOUT", 0.01),
|
|
378
380
|
pytest.raises(AuthenticationError, match="No login response"),
|
|
379
381
|
):
|
|
380
382
|
await connection.login()
|
|
@@ -700,11 +702,44 @@ async def test_login_skips_non_response(
|
|
|
700
702
|
connection._on_message({"Response": {"LoginResponse": {"status": "1"}}})
|
|
701
703
|
|
|
702
704
|
task = asyncio.create_task(_feed())
|
|
703
|
-
|
|
705
|
+
with patch("steamloop.connection.INITIAL_STATE_TIMEOUT", 0.05):
|
|
706
|
+
result = await connection.login()
|
|
704
707
|
assert result["status"] == "1"
|
|
705
708
|
await task
|
|
706
709
|
|
|
707
710
|
|
|
711
|
+
async def test_login_drains_initial_state(
|
|
712
|
+
connection: ThermostatConnection,
|
|
713
|
+
) -> None:
|
|
714
|
+
"""Login waits for the initial state burst after LoginResponse."""
|
|
715
|
+
|
|
716
|
+
async def _feed() -> None:
|
|
717
|
+
await asyncio.sleep(0.01)
|
|
718
|
+
connection._on_message({"Response": {"LoginResponse": {"status": "1"}}})
|
|
719
|
+
# Thermostat sends initial state burst after login response
|
|
720
|
+
await asyncio.sleep(0.01)
|
|
721
|
+
connection._on_message({"Event": {"ZoneAdded": {"zone_id": "1"}}})
|
|
722
|
+
connection._on_message(
|
|
723
|
+
{"Event": {"ZoneNameUpdated": {"zone_id": "1", "zone_name": "Main"}}}
|
|
724
|
+
)
|
|
725
|
+
await asyncio.sleep(0.01)
|
|
726
|
+
connection._on_message({"Event": {"ZoneAdded": {"zone_id": "2"}}})
|
|
727
|
+
connection._on_message(
|
|
728
|
+
{"Event": {"ZoneNameUpdated": {"zone_id": "2", "zone_name": "Upstairs"}}}
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
task = asyncio.create_task(_feed())
|
|
732
|
+
with patch("steamloop.connection.INITIAL_STATE_TIMEOUT", 0.15):
|
|
733
|
+
result = await connection.login()
|
|
734
|
+
assert result["status"] == "1"
|
|
735
|
+
# Both zones should be fully populated by the time login returns
|
|
736
|
+
assert "1" in connection.state.zones
|
|
737
|
+
assert "2" in connection.state.zones
|
|
738
|
+
assert connection.state.zones["1"].name == "Main"
|
|
739
|
+
assert connection.state.zones["2"].name == "Upstairs"
|
|
740
|
+
await task
|
|
741
|
+
|
|
742
|
+
|
|
708
743
|
# ---------------------------------------------------------------------------
|
|
709
744
|
# SSLCertVerificationError
|
|
710
745
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|