capyy 0.5.2__tar.gz → 0.5.3__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.
Files changed (27) hide show
  1. {capyy-0.5.2 → capyy-0.5.3}/PKG-INFO +14 -1
  2. {capyy-0.5.2 → capyy-0.5.3}/README.md +13 -0
  3. {capyy-0.5.2 → capyy-0.5.3}/capy/__main__.py +139 -2
  4. {capyy-0.5.2 → capyy-0.5.3}/capy/_project.py +1 -1
  5. {capyy-0.5.2 → capyy-0.5.3}/capy/_tls.py +3 -1
  6. {capyy-0.5.2 → capyy-0.5.3}/capyy.egg-info/PKG-INFO +14 -1
  7. {capyy-0.5.2 → capyy-0.5.3}/pyproject.toml +1 -1
  8. {capyy-0.5.2 → capyy-0.5.3}/tests/test_capy.py +54 -0
  9. {capyy-0.5.2 → capyy-0.5.3}/tests/test_domo.py +4 -1
  10. {capyy-0.5.2 → capyy-0.5.3}/LICENSE +0 -0
  11. {capyy-0.5.2 → capyy-0.5.3}/capy/__init__.py +0 -0
  12. {capyy-0.5.2 → capyy-0.5.3}/capy/domo/__init__.py +0 -0
  13. {capyy-0.5.2 → capyy-0.5.3}/capy/domo/__main__.py +0 -0
  14. {capyy-0.5.2 → capyy-0.5.3}/capy/domo/_base.py +0 -0
  15. {capyy-0.5.2 → capyy-0.5.3}/capy/domo/_homeassistant.py +0 -0
  16. {capyy-0.5.2 → capyy-0.5.3}/capy/domo/_hue.py +0 -0
  17. {capyy-0.5.2 → capyy-0.5.3}/capy/domo/_net.py +0 -0
  18. {capyy-0.5.2 → capyy-0.5.3}/capy/domo/_shelly.py +0 -0
  19. {capyy-0.5.2 → capyy-0.5.3}/capy/domo/_wiz.py +0 -0
  20. {capyy-0.5.2 → capyy-0.5.3}/capy/domo/_yeelight.py +0 -0
  21. {capyy-0.5.2 → capyy-0.5.3}/capy/network.py +0 -0
  22. {capyy-0.5.2 → capyy-0.5.3}/capy/wifi.py +0 -0
  23. {capyy-0.5.2 → capyy-0.5.3}/capyy.egg-info/SOURCES.txt +0 -0
  24. {capyy-0.5.2 → capyy-0.5.3}/capyy.egg-info/dependency_links.txt +0 -0
  25. {capyy-0.5.2 → capyy-0.5.3}/capyy.egg-info/entry_points.txt +0 -0
  26. {capyy-0.5.2 → capyy-0.5.3}/capyy.egg-info/top_level.txt +0 -0
  27. {capyy-0.5.2 → capyy-0.5.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capyy
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Easy, encrypted networking for Python: programs that talk (local, Wi-Fi, internet), home automation, and projects run with cy
5
5
  Author: Izac
6
6
  License-Expression: MIT
@@ -101,6 +101,19 @@ app.call("hello", nom="Izac") # client side
101
101
  `mode` can be `local`, `wifi` or `network` (with `relay`, `key`, `fingerprint`). Strings starting with `$` are read
102
102
  from environment variables, e.g. `"key": "$CAPY_KEY"`. Inside a project, the `cy` command runs its `scripts` like `npm run` (`cy test`, `cy start`) and its files (`cy server`); create a project with `capy new`.
103
103
 
104
+ ## From the terminal
105
+
106
+ ```bash
107
+ capy call localhost:4000 hello nom=Izac # any capy server (add --no-tls for a plain one)
108
+ capy wifi scan # capy.wifi servers on your network
109
+ capy wifi call home temperature
110
+ capy network call home test --relay my-vps.example:3879 --key "$CAPY_KEY"
111
+ cy call hello nom=Izac # inside a project: uses capy.config.json
112
+ ```
113
+
114
+ Parameters are `name=value`; numbers, `true`/`false` and lists are parsed (`age=14` sends the number 14).
115
+ The answer is printed as JSON; the exit code is 1 when the server answers `{"erreur": ...}`.
116
+
104
117
  ## Encryption (TLS)
105
118
 
106
119
  Everything is encrypted by default, with nothing to configure.
@@ -79,6 +79,19 @@ app.call("hello", nom="Izac") # client side
79
79
  `mode` can be `local`, `wifi` or `network` (with `relay`, `key`, `fingerprint`). Strings starting with `$` are read
80
80
  from environment variables, e.g. `"key": "$CAPY_KEY"`. Inside a project, the `cy` command runs its `scripts` like `npm run` (`cy test`, `cy start`) and its files (`cy server`); create a project with `capy new`.
81
81
 
82
+ ## From the terminal
83
+
84
+ ```bash
85
+ capy call localhost:4000 hello nom=Izac # any capy server (add --no-tls for a plain one)
86
+ capy wifi scan # capy.wifi servers on your network
87
+ capy wifi call home temperature
88
+ capy network call home test --relay my-vps.example:3879 --key "$CAPY_KEY"
89
+ cy call hello nom=Izac # inside a project: uses capy.config.json
90
+ ```
91
+
92
+ Parameters are `name=value`; numbers, `true`/`false` and lists are parsed (`age=14` sends the number 14).
93
+ The answer is printed as JSON; the exit code is 1 when the server answers `{"erreur": ...}`.
94
+
82
95
  ## Encryption (TLS)
83
96
 
84
97
  Everything is encrypted by default, with nothing to configure.
@@ -6,16 +6,21 @@
6
6
  # cy scripts -> liste les scripts
7
7
  # cy server / cy client -> lance un fichier de "files"
8
8
  # cy run -> lance le serveur en fond, puis le client, puis arrête le serveur
9
+ # cy call ACTION [nom=valeur…] -> appelle une action du serveur du projet (local, wifi ou network)
9
10
  #
10
11
  # capy : tout le reste (et tout ce que fait cy)
11
12
  # capy new NOM [mode] -> crée un dossier NOM avec capy.config.json, server.py, client.py
12
13
  # capy init [nom] [mode] -> crée juste capy.config.json ici
13
14
  # capy domo … -> la domotique (capy domo help)
14
15
  # capy relay PORT [CLÉ] -> lance un relais capy.network (sur le VPS)
16
+ # capy call HÔTE:PORT ACTION [nom=valeur…] -> appelle une action d'un serveur capy
17
+ # capy wifi scan / capy wifi call NOM ACTION … -> les serveurs du wifi
18
+ # capy network call NOM ACTION … [--relay R --key K] / capy network relay PORT [CLÉ]
15
19
  # mode : local (par défaut), wifi ou network
16
20
  # python3 -m capy = capy (si la commande n'est pas dans le PATH)
17
21
 
18
22
  import difflib
23
+ import json
19
24
  import socket
20
25
  import subprocess
21
26
  import sys
@@ -30,11 +35,18 @@ USAGE = f"""Dans un projet capy ({FILE_NAME}) :
30
35
  cy scripts liste les scripts
31
36
  cy server / client lance un fichier de "files"
32
37
  cy run serveur en fond + client, puis arrête le serveur
38
+ cy call ACTION [nom=valeur…] appelle une action du serveur du projet
33
39
  Partout :
34
40
  capy new NOM [mode] crée un projet complet (config, server.py, client.py)
35
41
  capy init [nom] [mode] crée juste {FILE_NAME} ici
36
42
  capy domo … la domotique (capy domo help)
37
43
  capy relay PORT [CLÉ] lance un relais capy.network
44
+ capy call HÔTE:PORT ACTION [nom=valeur…] [--no-tls]
45
+ capy wifi scan les serveurs capy du wifi
46
+ capy wifi call NOM ACTION [nom=valeur…] [--no-tls]
47
+ capy network call NOM ACTION [nom=valeur…] [--relay HÔTE:PORT] [--key CLÉ] [--fingerprint …]
48
+ capy network relay PORT [CLÉ] (= capy relay)
49
+ Paramètres : nom=Izac age=14 actif=true liste=[1,2] (les nombres, true/false et listes sont compris)
38
50
  modes : {", ".join(MODES)}"""
39
51
 
40
52
 
@@ -51,6 +63,118 @@ def _project():
51
63
  sys.exit(str(e))
52
64
 
53
65
 
66
+ # Sortir les options "--nom valeur" (et "--drapeau") d'une liste d'arguments -> (options, reste)
67
+ def _options(args, flags=(), values=()):
68
+ options, rest, i = {}, [], 0
69
+ while i < len(args):
70
+ word = args[i]
71
+ if word in flags:
72
+ options[word] = True
73
+ elif word in values:
74
+ if i + 1 >= len(args):
75
+ sys.exit(f"capy : il manque une valeur après {word}")
76
+ options[word] = args[i + 1]
77
+ i += 1
78
+ elif word.startswith("--"):
79
+ sys.exit(f"capy : option {word} inconnue (possibles : {', '.join((*flags, *values)) or 'aucune'})")
80
+ else:
81
+ rest.append(word)
82
+ i += 1
83
+ return options, rest
84
+
85
+
86
+ # "nom=Izac" "age=14" -> {"nom": "Izac", "age": 14} : une valeur qui est du JSON (14, true, [1, 2]) est convertie
87
+ def _params(words):
88
+ params = {}
89
+ for word in words:
90
+ key, sep, value = word.partition("=")
91
+ if not sep or not key:
92
+ sys.exit(f"capy : paramètre {word!r} invalide, écris nom=valeur (ex : nom=Izac age=14)")
93
+ try:
94
+ params[key] = json.loads(value)
95
+ except ValueError:
96
+ params[key] = value
97
+ return params
98
+
99
+
100
+ # Faire l'appel et afficher la réponse en JSON ; code de sortie 1 si erreur (pratique dans un script)
101
+ def _call(function):
102
+ import capy
103
+ try:
104
+ answer = function()
105
+ except (ConnectionError, ValueError, KeyError, capy.SecurityError) as e:
106
+ sys.exit(str(e).strip("'\""))
107
+ print(json.dumps(answer, indent=2, ensure_ascii=False))
108
+ if isinstance(answer, dict) and "erreur" in answer:
109
+ sys.exit(1)
110
+
111
+
112
+ USAGE_CALL = "Usage : capy call HÔTE:PORT ACTION [nom=valeur…] [--no-tls] (ex : capy call localhost:4000 hello nom=Izac)"
113
+
114
+
115
+ # capy call localhost:4000 hello nom=Izac
116
+ def call_direct(args):
117
+ import capy
118
+ options, rest = _options(args, flags=("--no-tls",))
119
+ if len(rest) < 2:
120
+ sys.exit(USAGE_CALL)
121
+ host, _, port = rest[0].rpartition(":")
122
+ if not host or not port.isdigit():
123
+ sys.exit(f"capy : adresse {rest[0]!r} invalide, écris HÔTE:PORT (ex : localhost:4000)\n{USAGE_CALL}")
124
+ _call(lambda: capy.call(host, int(port), rest[1], tls="--no-tls" not in options, **_params(rest[2:])))
125
+
126
+
127
+ # capy wifi scan / capy wifi call maison hello nom=Izac
128
+ def wifi(args):
129
+ import capy.wifi
130
+ sub = args[0] if args else None
131
+ if sub == "scan":
132
+ options, _ = _options(args[1:], values=("--timeout",))
133
+ found = capy.wifi.scan(timeout=float(options.get("--timeout", 1)))
134
+ if not found:
135
+ sys.exit("Aucun serveur capy.wifi trouvé (même wifi ? wifi public qui isole les appareils ?)")
136
+ for name, (ip, port) in sorted(found.items()):
137
+ print(f" {name:<20} {ip}:{port}{' (cet appareil)' if ip == '127.0.0.1' else ''}")
138
+ elif sub == "call":
139
+ options, rest = _options(args[1:], flags=("--no-tls",))
140
+ if len(rest) < 2:
141
+ sys.exit("Usage : capy wifi call NOM ACTION [nom=valeur…] [--no-tls] (ex : capy wifi call maison hello)")
142
+ _call(lambda: capy.wifi.call(rest[0], rest[1], tls="--no-tls" not in options, **_params(rest[2:])))
143
+ else:
144
+ sys.exit("Usage : capy wifi scan [--timeout S] ou capy wifi call NOM ACTION [nom=valeur…] [--no-tls]")
145
+
146
+
147
+ # capy network call maison hello --relay mon-vps.fr:3879 --key … / capy network relay 3879 CLÉ
148
+ # Relais et clé : options, sinon le capy.config.json du dossier (mode network), sinon $CAPY_RELAY / $CAPY_KEY
149
+ def network(args):
150
+ import os
151
+
152
+ import capy.network
153
+ sub = args[0] if args else None
154
+ if sub == "relay" or (sub or "").isdigit(): # capy network relay 3879 CLÉ (ou l'ancien capy network 3879 CLÉ)
155
+ capy.network.cli(args[1:] if sub == "relay" else args)
156
+ elif sub == "call":
157
+ options, rest = _options(args[1:], values=("--relay", "--key", "--fingerprint"))
158
+ if len(rest) < 2:
159
+ sys.exit("Usage : capy network call NOM ACTION [nom=valeur…] [--relay HÔTE:PORT] [--key CLÉ] [--fingerprint …]")
160
+ relay, key, fingerprint = options.get("--relay"), options.get("--key"), options.get("--fingerprint")
161
+ path = find(".")
162
+ if path and not relay:
163
+ try:
164
+ app = project(path)
165
+ except ValueError:
166
+ app = None
167
+ if app and app.mode == "network":
168
+ relay, key, fingerprint = app.relay, key or app.key, fingerprint or app.fingerprint
169
+ relay, key = relay or os.environ.get("CAPY_RELAY"), key or os.environ.get("CAPY_KEY")
170
+ if not relay:
171
+ sys.exit("capy : quel relais ? Ajoute --relay mon-vps.fr:3879 (ou un capy.config.json en mode network, ou $CAPY_RELAY)")
172
+ capy.network.use(relay, key=key, fingerprint=fingerprint)
173
+ _call(lambda: capy.network.call(rest[0], rest[1], **_params(rest[2:])))
174
+ else:
175
+ sys.exit("Usage : capy network call NOM ACTION [nom=valeur…] [--relay …] [--key …] ou capy network relay PORT [CLÉ]")
176
+
177
+
54
178
  # Lancer un fichier du projet avec le même Python, depuis le dossier du projet
55
179
  def _start(app, name, args=(), **options):
56
180
  try:
@@ -120,10 +244,19 @@ def main(args):
120
244
  from .domo.__main__ import cli as domo_cli
121
245
  domo_cli(args[1:])
122
246
  return
123
- if command in ("relay", "network"): # capy relay 3879 CLÉ
247
+ if command == "relay": # capy relay 3879 CLÉ
124
248
  from .network import cli as relay_cli
125
249
  relay_cli(args[1:])
126
250
  return
251
+ if command == "network":
252
+ network(args[1:])
253
+ return
254
+ if command == "wifi":
255
+ wifi(args[1:])
256
+ return
257
+ if command == "call" and len(args) > 1 and ":" in args[1]: # capy call localhost:4000 hello
258
+ call_direct(args[1:])
259
+ return
127
260
  if command in ("init", "new"):
128
261
  rest = args[1:]
129
262
  mode = next((a for a in rest if a in MODES), "local")
@@ -146,6 +279,10 @@ def main(args):
146
279
  print(app.describe())
147
280
  elif command == "run":
148
281
  run(app, args[1:])
282
+ elif command == "call": # cy call hello nom=Izac : le serveur du projet, quel que soit son mode
283
+ if len(args) < 2:
284
+ sys.exit("Usage : cy call ACTION [nom=valeur…] (ex : cy call hello nom=Izac)")
285
+ _call(lambda: app.call(args[1], **_params(args[2:])))
149
286
  elif command == "scripts":
150
287
  if not app.scripts:
151
288
  print('Pas de "scripts" dans ' + FILE_NAME + ', ex : "scripts": {"test": "python3 -m unittest"}')
@@ -173,7 +310,7 @@ def main(args):
173
310
  # La commande cy : seulement les commandes d'un projet
174
311
  def cli():
175
312
  args = sys.argv[1:]
176
- if args[:1] in (["new"], ["init"], ["domo"], ["relay"], ["network"]):
313
+ if args[:1] in (["new"], ["init"], ["domo"], ["relay"], ["network"], ["wifi"]):
177
314
  sys.exit(f"cy sert dans un projet capy ; pour ça, c'est la commande capy : capy {' '.join(args)}")
178
315
  main(args)
179
316
 
@@ -46,7 +46,7 @@ FIELDS = {
46
46
 
47
47
  EXAMPLE = {"name": "mon-projet", "mode": "local", "host": "localhost", "port": 4000, "tls": True,
48
48
  "files": {"server": "server.py", "client": "client.py"}}
49
- COMMANDS = ("init", "new", "run", "scripts", "help", "domo", "relay") # Mots réservés de cy / capy (pas des noms de fichiers ni de scripts)
49
+ COMMANDS = ("init", "new", "run", "scripts", "help", "call", "domo", "relay", "wifi", "network") # Mots réservés de cy / capy (pas des noms de fichiers ni de scripts)
50
50
 
51
51
 
52
52
  # Dossier du script lancé (comme simplesuit.json) ; sans script (python3 -c, REPL) -> dossier actuel
@@ -12,6 +12,7 @@ import pathlib as pth
12
12
  import socket
13
13
  import ssl
14
14
  import subprocess
15
+ import sys
15
16
  import threading
16
17
 
17
18
  CAPY_DIR = pth.Path.home() / ".capy"
@@ -119,7 +120,8 @@ def check(label, seen, pinned=None, who="capy", kind="serveur", local=False):
119
120
  CAPY_DIR.mkdir(exist_ok=True)
120
121
  known[label] = seen
121
122
  KNOWN_FILE.write_text(json.dumps(known, indent=4))
122
- print(f"{who} : nouveau {kind} {label}, empreinte retenue {seen}", flush=True)
123
+ # Sur stderr : la sortie normale reste propre (ex : capy call … | jq)
124
+ print(f"{who} : nouveau {kind} {label}, empreinte retenue {seen}", file=sys.stderr, flush=True)
123
125
  return
124
126
  if not hmac.compare_digest(known[label], seen):
125
127
  raise SecurityError(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capyy
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Easy, encrypted networking for Python: programs that talk (local, Wi-Fi, internet), home automation, and projects run with cy
5
5
  Author: Izac
6
6
  License-Expression: MIT
@@ -101,6 +101,19 @@ app.call("hello", nom="Izac") # client side
101
101
  `mode` can be `local`, `wifi` or `network` (with `relay`, `key`, `fingerprint`). Strings starting with `$` are read
102
102
  from environment variables, e.g. `"key": "$CAPY_KEY"`. Inside a project, the `cy` command runs its `scripts` like `npm run` (`cy test`, `cy start`) and its files (`cy server`); create a project with `capy new`.
103
103
 
104
+ ## From the terminal
105
+
106
+ ```bash
107
+ capy call localhost:4000 hello nom=Izac # any capy server (add --no-tls for a plain one)
108
+ capy wifi scan # capy.wifi servers on your network
109
+ capy wifi call home temperature
110
+ capy network call home test --relay my-vps.example:3879 --key "$CAPY_KEY"
111
+ cy call hello nom=Izac # inside a project: uses capy.config.json
112
+ ```
113
+
114
+ Parameters are `name=value`; numbers, `true`/`false` and lists are parsed (`age=14` sends the number 14).
115
+ The answer is printed as JSON; the exit code is 1 when the server answers `{"erreur": ...}`.
116
+
104
117
  ## Encryption (TLS)
105
118
 
106
119
  Everything is encrypted by default, with nothing to configure.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "capyy"
7
- version = "0.5.2"
7
+ version = "0.5.3"
8
8
  description = "Easy, encrypted networking for Python: programs that talk (local, Wi-Fi, internet), home automation, and projects run with cy"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -357,5 +357,59 @@ class TestProject(unittest.TestCase):
357
357
  self.assertEqual(json.loads(path.read_text())["key"], "$CAPY_KEY")
358
358
 
359
359
 
360
+ # Les commandes capy / cy, lancées pour de vrai (dossier personnel temporaire : ~/.capy n'est pas touché)
361
+ class TestCommandLine(unittest.TestCase):
362
+ @classmethod
363
+ def setUpClass(cls):
364
+ cls.home = tempfile.TemporaryDirectory()
365
+ cls.server = start(demo_server(capy.Server(0, tls=False)))
366
+ cls.relay_port = free_port()
367
+ threading.Thread(target=capy.network.Relay(cls.relay_port, key="cle").run, daemon=True).start()
368
+ time.sleep(0.5)
369
+ capy.network.use(f"127.0.0.1:{cls.relay_port}", key="cle")
370
+ start(demo_server(capy.network.Server("cli")), wait=lambda s: True)
371
+ time.sleep(1)
372
+
373
+ @classmethod
374
+ def tearDownClass(cls):
375
+ cls.home.cleanup()
376
+
377
+ def run_capy(self, *args, cwd=None):
378
+ import subprocess
379
+ import sys
380
+ # Autre HOME : Python ne voit plus les paquets installés pour l'utilisateur, on lui montre capy
381
+ env = {**os.environ, "HOME": self.home.name, "PYTHONPATH": str(pathlib.Path(__file__).resolve().parent.parent)}
382
+ return subprocess.run([sys.executable, "-m", "capy", *args], cwd=cwd or self.home.name, env=env,
383
+ capture_output=True, text=True, timeout=30)
384
+
385
+ def test_call(self):
386
+ result = self.run_capy("call", f"localhost:{self.server.port}", "hello", "name=Zoé", "--no-tls")
387
+ self.assertEqual((result.returncode, json.loads(result.stdout)), (0, {"message": "Bonjour Zoé"}))
388
+ result = self.run_capy("call", f"localhost:{self.server.port}", "divide", "a=6", "b=3", "--no-tls")
389
+ self.assertEqual(json.loads(result.stdout), {"result": 2.0}) # "6" et "3" compris comme des nombres
390
+ result = self.run_capy("call", f"localhost:{self.server.port}", "nope", "--no-tls")
391
+ self.assertEqual(result.returncode, 1) # Réponse {"erreur": ...} -> code 1
392
+ self.assertIn("nom=valeur", self.run_capy("call", f"localhost:{self.server.port}", "hello", "name", "--no-tls").stderr)
393
+
394
+ def test_project_call(self):
395
+ folder = pathlib.Path(self.home.name) / "projet"
396
+ folder.mkdir(exist_ok=True)
397
+ (folder / "capy.config.json").write_text(json.dumps({"port": self.server.port, "tls": False}))
398
+ result = self.run_capy("call", "hello", "name=Projet", cwd=folder)
399
+ self.assertEqual(json.loads(result.stdout), {"message": "Bonjour Projet"})
400
+
401
+ def test_network_call(self):
402
+ result = self.run_capy("network", "call", "cli", "hello", "name=Léa", "--relay", f"127.0.0.1:{self.relay_port}", "--key", "cle")
403
+ self.assertEqual(json.loads(result.stdout), {"message": "Bonjour Léa"})
404
+ self.assertIn("quel relais", self.run_capy("network", "call", "cli", "hello").stderr)
405
+
406
+ def test_reserved_script_names(self):
407
+ for name in ("call", "wifi", "network"):
408
+ with self.assertRaisesRegex(ValueError, "nom d'une commande"):
409
+ path = pathlib.Path(self.home.name) / "capy.config.json"
410
+ path.write_text(json.dumps({"port": 1, "scripts": {name: "echo"}}))
411
+ capy.project(path)
412
+
413
+
360
414
  if __name__ == "__main__":
361
415
  unittest.main()
@@ -345,7 +345,10 @@ class TestBrands(unittest.TestCase):
345
345
  self.assertFalse(found["switch.radiateur"].state()["on"])
346
346
 
347
347
  def test_unreachable(self):
348
- lamp = domo._wiz.WizLight("fantome", "127.0.0.1", port=free_port(socket.SOCK_DGRAM))
348
+ silent = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Un port gardé, qui ne répond jamais
349
+ silent.bind(("127.0.0.1", 0))
350
+ self.addCleanup(silent.close)
351
+ lamp = domo._wiz.WizLight("fantome", "127.0.0.1", port=silent.getsockname()[1])
349
352
  self.assertEqual(lamp.state()["reachable"], False)
350
353
  with self.assertRaisesRegex(ConnectionError, "'fantome' \\(wiz\\) ne répond pas"):
351
354
  lamp.on()
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