wirely 0.1.0__py3-none-any.whl

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 (59) hide show
  1. wirely/__init__.py +406 -0
  2. wirely/__main__.py +45 -0
  3. wirely/ast.py +1135 -0
  4. wirely/block.py +542 -0
  5. wirely/block_type.py +342 -0
  6. wirely/blocks.lark +126 -0
  7. wirely/compiler.py +125 -0
  8. wirely/engine.py +418 -0
  9. wirely/enum.py +15 -0
  10. wirely/example.py +36 -0
  11. wirely/graphviz.py +107 -0
  12. wirely/knx/__init__.py +0 -0
  13. wirely/knx/connection_adapter.py +90 -0
  14. wirely/knx/connection_config.py +152 -0
  15. wirely/lib/abb_jra.wrl +64 -0
  16. wirely/lib/csv.wrl +32 -0
  17. wirely/lib/ext_prog.wrl +12 -0
  18. wirely/lib/gpio.wrl +23 -0
  19. wirely/lib/knx.wrl +80 -0
  20. wirely/lib/memory.wrl +45 -0
  21. wirely/lib/mqtt.wrl +66 -0
  22. wirely/lib/std.wrl +134 -0
  23. wirely/lib/time.wrl +93 -0
  24. wirely/lib/two_n.wrl +111 -0
  25. wirely/misc/__init__.py +0 -0
  26. wirely/misc/service.py +48 -0
  27. wirely/native/__init__.py +74 -0
  28. wirely/native/abb_jra.py +66 -0
  29. wirely/native/csv.py +194 -0
  30. wirely/native/ext_prog.py +77 -0
  31. wirely/native/gpio.py +125 -0
  32. wirely/native/knx.py +455 -0
  33. wirely/native/memory.py +113 -0
  34. wirely/native/mqtt.py +605 -0
  35. wirely/native/std.py +334 -0
  36. wirely/native/test_library.py +14 -0
  37. wirely/native/time.py +359 -0
  38. wirely/native/two_n.py +632 -0
  39. wirely/node.py +125 -0
  40. wirely/parser.py +175 -0
  41. wirely/program.py +155 -0
  42. wirely/run.py +143 -0
  43. wirely/service.py +65 -0
  44. wirely/test.py +569 -0
  45. wirely/time.py +172 -0
  46. wirely/two_n/__init__.py +0 -0
  47. wirely/two_n/cert.py +126 -0
  48. wirely/two_n/client.py +402 -0
  49. wirely/two_n/fake_server.py +415 -0
  50. wirely/two_n/grab_images.py +146 -0
  51. wirely/types.py +522 -0
  52. wirely/util.py +111 -0
  53. wirely/value.py +163 -0
  54. wirely-0.1.0.dist-info/METADATA +94 -0
  55. wirely-0.1.0.dist-info/RECORD +59 -0
  56. wirely-0.1.0.dist-info/WHEEL +5 -0
  57. wirely-0.1.0.dist-info/entry_points.txt +2 -0
  58. wirely-0.1.0.dist-info/licenses/LICENSE +21 -0
  59. wirely-0.1.0.dist-info/top_level.txt +1 -0
wirely/engine.py ADDED
@@ -0,0 +1,418 @@
1
+ import asyncio
2
+ import contextlib
3
+ import datetime
4
+
5
+ import wirely as blocks
6
+ from wirely.util import check_parameters
7
+
8
+ class Engine:
9
+ def __init__(self, program):
10
+ if not isinstance(program, blocks.Program):
11
+ raise Exception('program parameter is not a Program')
12
+
13
+ self.program = program
14
+
15
+ self.cache = None
16
+
17
+ self.debug = False
18
+
19
+ # Is a datetime during async task shutdown unless the shutdown was triggered before all async tasks were started
20
+ self.shutdown_time = None
21
+
22
+ # The time of the last complete async task shutdown. Should be used only for debugging / tests.
23
+ self.last_shutdown_time = None
24
+
25
+ self.triggered_procs = set()
26
+ self.triggered_procs_event = asyncio.Event()
27
+
28
+ self.wakeups = {}
29
+ self.next_wakeup = None
30
+
31
+ self.state = blocks.VariableBlockInstance(self.program.state_variables)
32
+ self.native_states = {}
33
+
34
+ self.fake_monotonic_time_offset = None
35
+
36
+ # TODO: Clean up iterating over all blocks paths
37
+ done_block_path = set()
38
+ for block_name in self.program.graph.blocks:
39
+ for block_path in self.program.graph.blocks[block_name].get_all_block_paths(()):
40
+ if block_path in done_block_path:
41
+ if len(block_path) != 1 or not isinstance(block_path[-1], blocks.BlockType):
42
+ raise Exception('Got duplicate non-singleton block')
43
+ continue
44
+ done_block_path.add(block_path)
45
+
46
+ block = block_path[-1]
47
+ instance = self.program.instances[block_path]
48
+ # TODO: Clean up
49
+ if instance.is_singleton:
50
+ if block != instance.block_type:
51
+ raise Exception('block != instance.block_type')
52
+ else:
53
+ if block != instance.block:
54
+ raise Exception('block != instance.block')
55
+
56
+ # TODO: Pass full context to native state constructor?
57
+ self.native_states[block_path] = block.create_native_state(block_path, self, static_block_context=instance.static_context)
58
+ # TODO: Remove hack
59
+ if not isinstance(block, blocks.Block):
60
+ block = block.singleton_instance
61
+ for var in block.block_statevars:
62
+ self.state.add(self.program.state_map[block_path, var], var.initial_value)
63
+
64
+ async def start_tasks_async(self, task_group, async_exit_stack: contextlib.AsyncExitStack):
65
+ def start_task_cleanup():
66
+ now_monotonic = blocks.MonotonicTime.current_time()
67
+ if self.fake_monotonic_time_offset is not None:
68
+ now_monotonic += self.fake_monotonic_time_offset
69
+
70
+ now_utc = datetime.datetime.now(datetime.timezone.utc)
71
+ now = now_utc.astimezone()
72
+
73
+ print(f'Task cleanup starting at {now}')
74
+
75
+ # TODO: Set monotonic_shutdown_time?
76
+ self.shutdown_time = now
77
+
78
+ def finish_task_cleanup():
79
+ print('Task cleanup done')
80
+
81
+ self.last_shutdown_time = self.shutdown_time
82
+ self.shutdown_time = None
83
+
84
+ async_exit_stack.callback(finish_task_cleanup)
85
+
86
+ # TODO: Currently this (e.g. connecting to knxd) is blocking and done sequentially. Should ths be non-blocking instead? (I.e. should the connection be created on the background task and should start_tasks_async() be removed?)
87
+ for state in self.native_states.values():
88
+ if state is not None:
89
+ state.start_tasks(task_group, async_exit_stack)
90
+ await state.start_tasks_async(task_group, async_exit_stack)
91
+
92
+ async_exit_stack.callback(start_task_cleanup)
93
+
94
+ # TODO: Threading?
95
+ def trigger(self, path, proc):
96
+ # TODO: More checks with path
97
+ if not isinstance(proc, blocks.Block.Procedure):
98
+ raise Exception('proc parameter is not a Block.Procedure')
99
+
100
+ if (path, proc) not in self.program.all_procs_set:
101
+ # print(self.program.all_procs_set)
102
+ raise Exception(f'Got procedure {path} {proc} which is not in {self.program.graph}')
103
+
104
+ self.triggered_procs.add((path, proc))
105
+ self.triggered_procs_event.set()
106
+
107
+ # Update self.next_wakeup to include wakeup_time
108
+ def update_next_wakeup(self, now, wakeup_time):
109
+ if wakeup_time is not None and not wakeup_time.is_invalid and not blocks.MonotonicTime.is_comparable(now, wakeup_time):
110
+ print('Warning: Got non-current wakeup time {wakeup_time}', flush=True)
111
+ wakeup_time = blocks.MonotonicTime.create_invalid()
112
+ if wakeup_time is None:
113
+ return
114
+
115
+ if self.next_wakeup is None or wakeup_time.is_invalid or blocks.MonotonicTime.compare(self.next_wakeup, wakeup_time) > 0:
116
+ self.next_wakeup = wakeup_time
117
+
118
+ def step(self):
119
+ if self.debug:
120
+ print(f'Starting step of {self.program.graph}')
121
+ # print(f'Wakeups: {self.wakeups}')
122
+
123
+ self.next_wakeup = None
124
+
125
+ cache = self.cache
126
+ state_old = self.state
127
+ state_new = blocks.VariableBlockInstance(self.program.state_variables)
128
+ vars = blocks.VariableBlockInstance(self.program.temporaries)
129
+ run_count = 0
130
+ change_count = 0
131
+ state_change_count = 0
132
+ step_info = blocks.StepContext()
133
+ for stage, stage_procs in self.program.procs.items():
134
+ # print(f'Stage {stage}', flush=True)
135
+ step_info.start_stage(self, stage)
136
+
137
+ now = step_info.stage_start_time_monotonic[stage]
138
+
139
+ for path, proc in stage_procs:
140
+ if path[-1] != proc.block:
141
+ if isinstance(path[-1], blocks.Block): # TODO: Handle singletons here?
142
+ raise Exception('path[-1] != proc.block')
143
+ native_state = self.native_states[path]
144
+
145
+ # print(proc)
146
+ deps = {}
147
+ skip_step = cache is not None and (path, proc) not in self.triggered_procs
148
+ for dep in proc.dependencies:
149
+ if isinstance(dep, blocks.Block.StateVariable):
150
+ # TODO
151
+ raise Exception('isinstance(dep, blocks.Block.StateVariable)')
152
+ if skip_step and not dep.impl_var_type.equals(state_old[self.program.state_map[path, dep]], cache[dep]):
153
+ skip_step = False
154
+ elif isinstance(dep, blocks.Block.Input) and dep.is_list:
155
+ for child in dep.members:
156
+ # TODO: Move this code somewhere else? / Use already resolved type?
157
+ var_type = child.impl_var_type
158
+ for block in reversed(path):
159
+ var_type = var_type.resolve_self(block.par_values)
160
+
161
+ deps[child] = vars[self.program.variable_map[path, child.var_identifier]]
162
+ if skip_step and not var_type.equals(vars[self.program.variable_map[path, child.var_identifier]], cache[self.program.variable_map[path, child.var_identifier]]):
163
+ skip_step = False
164
+ else:
165
+ # TODO: Move this code somewhere else? / Use already resolved type?
166
+ var_type = dep.impl_var_type
167
+ for block in reversed(path):
168
+ var_type = var_type.resolve_self(block.par_values)
169
+
170
+ # TODO: Program should contain some mapping from a procedures dependencies to variables, this should not be done here
171
+ # print(path, dep, dep.var_identifier)
172
+ if isinstance(path[-1], blocks.Block) and path[-1] != dep.block:
173
+ if dep.block.block_type.is_singleton:
174
+ variable = self.program.variable_map[(dep.block.block_type,), dep.var_identifier]
175
+ else:
176
+ # TODO: This assumes that dep.block is a direct child of the current block, this will not work otherwise
177
+ variable = self.program.variable_map[path + (dep.block,), dep.var_identifier]
178
+ else:
179
+ variable = self.program.variable_map[path, dep.var_identifier]
180
+
181
+ deps[dep] = vars[variable]
182
+ if skip_step and not var_type.equals(vars[variable], cache[variable]):
183
+ skip_step = False
184
+ # TODO: Avoid depending on the value of all state variables?
185
+ for dep in proc.block.block_statevars:
186
+ if skip_step and cache[self.program.state_map_changed[path, dep]]:
187
+ skip_step = False
188
+ res = {}
189
+
190
+ if (path, proc) in self.wakeups:
191
+ if not blocks.MonotonicTime.is_comparable(now, self.wakeups[(path, proc)]) or blocks.MonotonicTime.compare(now, self.wakeups[(path, proc)]) >= 0:
192
+ # Timeout, run step
193
+ skip_step = False
194
+
195
+ if skip_step:
196
+ for res_var in proc.results:
197
+ if isinstance(res_var, blocks.Block.StateVariable):
198
+ old_value = state_old[self.program.state_map[path, res_var]]
199
+ state_new.add(self.program.state_map[path, res_var], old_value)
200
+ vars.add(self.program.state_map_new[path, res_var], old_value)
201
+ vars.add(self.program.state_map_changed[path, res_var], False)
202
+ else:
203
+ # TODO: Clean up temporary / output stuff
204
+ vars.add(self.program.variable_map[path, res_var.var_identifier], cache[self.program.variable_map[path, res_var.var_identifier]])
205
+
206
+ if (path, proc) in self.wakeups:
207
+ self.update_next_wakeup(now, self.wakeups[(path, proc)])
208
+ continue
209
+
210
+ if self.debug:
211
+ print(f'Running procedure {path} {proc}')
212
+
213
+ run_count += 1
214
+
215
+ if (path, proc) in self.triggered_procs:
216
+ self.triggered_procs.remove((path, proc))
217
+
218
+ state_old_block = {}
219
+ # TODO: Remove state_new_block, use res instead?
220
+ state_new_block = {}
221
+ for var in proc.block.block_statevars:
222
+ state_old_block[var] = state_old[self.program.state_map[path, var]]
223
+ # TODO: Should this be initialized here?
224
+ # state_new_block[var] = state_old[var]
225
+
226
+ variables = None # TODO
227
+ instance = self.program.instances[path]
228
+ pcontext = blocks.ProcedureContext(dep_values=deps, results=res, state_old=state_old_block, state_new=state_new_block, static=self.program.proc_static_contexts[(instance, proc)], step_info=step_info)
229
+ proc.impl(context=pcontext, native_state=native_state)
230
+
231
+ if pcontext.wakeup_time is None:
232
+ if (path, proc) in self.wakeups:
233
+ del self.wakeups[(path, proc)]
234
+ else:
235
+ self.wakeups[(path, proc)] = pcontext.wakeup_time
236
+ self.update_next_wakeup(now=now, wakeup_time=pcontext.wakeup_time)
237
+
238
+ unknown = set(res)
239
+ unknown_newstate = set(state_new_block)
240
+ # TODO: Clean up, more checking for state variables
241
+ for res_var in proc.results:
242
+ if isinstance(res_var, blocks.Block.StateVariable):
243
+ if res_var not in unknown_newstate:
244
+ raise Exception(f'State output {res_var.name} of native procedure {proc} is missing')
245
+ unknown_newstate.remove(res_var)
246
+ val = state_new_block[res_var]
247
+ state_new.add(self.program.state_map[path, res_var], val)
248
+
249
+ new_value = res_var.impl_var_type.verify_value(val)
250
+ vars.add(self.program.state_map_new[path, res_var], state_old_block[res_var])
251
+ if not res_var.impl_var_type.equals(state_old_block[res_var], new_value):
252
+ state_change_count += 1
253
+ vars.add(self.program.state_map_changed[path, res_var], True)
254
+ else:
255
+ vars.add(self.program.state_map_changed[path, res_var], False)
256
+
257
+ continue
258
+
259
+ if res_var not in unknown:
260
+ raise Exception(f'Output {res_var.name_desc} of native procedure {proc} is missing')
261
+ unknown.remove(res_var)
262
+ val = res[res_var]
263
+ # print(f'Writing {res_var}')
264
+ # TODO: Clean up temporary / output stuff
265
+
266
+ # TODO: Move this code somewhere else? / Use already resolved type?
267
+ var_type = res_var.impl_var_type
268
+ for block in reversed(path):
269
+ if isinstance(block, blocks.Block): # TODO: Handle singletons here?
270
+ var_type = var_type.resolve_self(block.par_values)
271
+
272
+ # print('RESULT', proc.block.block_type, proc, res_var)
273
+ new_value = var_type.verify_value(val)
274
+ if cache is None or not var_type.equals(new_value, cache[self.program.variable_map[path, res_var.var_identifier]]):
275
+ change_count += 1
276
+ # print(self.program.variable_map, [path, res_var.var_identifier])
277
+ vars.add(self.program.variable_map[path, res_var.var_identifier], new_value)
278
+ if len(unknown):
279
+ raise Exception(f'{proc} returned unknown values: {sorted(unknown)}')
280
+ if len(unknown_newstate):
281
+ raise Exception(f'{proc} returned unknown state values: {sorted(unknown_newstate)}')
282
+
283
+ self.cache = vars
284
+ self.state = state_new
285
+
286
+ if self.debug:
287
+ print(f'Finished step of {self.program.graph}')
288
+
289
+ # print(state_old, state_new)
290
+ # print(run_count, change_count, state_change_count)
291
+ # return run_count
292
+ # return change_count
293
+ # TODO: Clean up return values
294
+ return run_count, state_change_count, step_info
295
+
296
+ async def run(self):
297
+ while True:
298
+ await asyncio.sleep(0)
299
+ run_count, state_change_count, step_info = self.step()
300
+ if state_change_count == 0:
301
+ now = blocks.MonotonicTime.current_time()
302
+ # TODO: This probably is not really used or tested currently
303
+ if self.fake_monotonic_time_offset is not None:
304
+ now_monotonic += self.fake_monotonic_time_offset
305
+
306
+ # TODO: If there is a "while True" here, the code will crash (instead of looping) because of EAGAIN when writing to the terminal, seems like NONBLOCK is set
307
+ # while True:
308
+ # import os; import fcntl; fcntl.fcntl(1, fcntl.F_GETFL)
309
+ # print(f'Waiting, wakeup is {self.next_wakeup}', flush=True)
310
+ # import os; print(os.
311
+ if self.next_wakeup is None:
312
+ # Sleep without timeout
313
+ await self.triggered_procs_event.wait()
314
+ elif self.next_wakeup.is_invalid or not blocks.MonotonicTime.is_comparable(now, self.next_wakeup):
315
+ # Don't sleep
316
+ pass
317
+ else:
318
+ diff = self.next_wakeup - now
319
+ # Sleep until timeout
320
+ try:
321
+ async with asyncio.timeout(diff.total_seconds):
322
+ await self.triggered_procs_event.wait()
323
+ except TimeoutError:
324
+ pass
325
+ self.triggered_procs_event.clear()
326
+
327
+ def get_block_path_list(self, name_parts):
328
+ path = []
329
+ graph = self.program.graph
330
+ for part in name_parts:
331
+ # print('XXX', graph, part, list(graph.blocks))
332
+ if part not in graph.blocks:
333
+ # TODO: Clean up
334
+ # raise Exception('Cannot find block ' + '.'.join([block.name for block in path] + [part]))
335
+ raise Exception('Cannot find block ' + '.'.join([(block.name if isinstance(block, blocks.Block) else block.name.id_str) for block in path] + [part]))
336
+ block = graph.blocks[part]
337
+ path.append(block)
338
+ graph = block.block_type
339
+
340
+ # For singletons, return the block type
341
+ if path[-1].block_type.is_singleton:
342
+ return (path[-1].block_type,)
343
+
344
+ return tuple(path)
345
+
346
+ def get_block_path(self, name):
347
+ return self.get_block_path_list(name.split('.'))
348
+
349
+ def get_block_path_singleton(self, name):
350
+ scope = self.program.graph
351
+ parts = name.split('.')
352
+ for part in parts[:-1]:
353
+ scope = scope.libraries[part]
354
+ ty = scope.block_type_implementations[parts[-1]]
355
+ return (ty,)
356
+
357
+ # TODO: Name? get_native_state? get_native_block? get_block_instance?
358
+ def get_native_state(self, name):
359
+ path = self.get_block_path(name)
360
+ return self.native_states[path]
361
+
362
+ def get_native_state_singleton(self, name):
363
+ path = self.get_block_path_singleton(name)
364
+ return self.native_states[path]
365
+
366
+ def get_variable(self, name):
367
+ name_parts = name.split('.')
368
+
369
+ # TODO: Avoid special case
370
+ if len(name_parts) == 1:
371
+ path = self.get_block_path_list(name_parts)
372
+ block = path[-1]
373
+ # return blocks.resolve_variable(path, block.block_outputs_byname[''])
374
+ path, var = blocks.resolve_variable(path, block.block_outputs_byname[''])
375
+ return path, var.var_identifier
376
+
377
+ path = self.get_block_path_list(name_parts[:-1])
378
+ block = path[-1]
379
+ lname = name_parts[-1]
380
+
381
+ # TODO: Clean this up
382
+ # TODO: How to handle input/output clashes?
383
+ for value in block.block_inputs:
384
+ if value.name == lname:
385
+ return path, value.var_identifier
386
+ for value in block.block_outputs:
387
+ if value.name == lname:
388
+ return path, value.var_identifier
389
+ for value in block.block_statevars:
390
+ if value.name == lname:
391
+ return path, value.var_identifier
392
+ for value in block.block_temporaries:
393
+ if value.name == lname:
394
+ return path, value.var_identifier
395
+
396
+ # print(block.block_inputs, block.block_temporaries, block.block_statevars)
397
+ raise Exception(f'Could not find {name}')
398
+
399
+
400
+ def get_value(self, name):
401
+ var = self.program.variable_map[self.get_variable(name)]
402
+ return self.cache[var]
403
+
404
+ def assert_value_is(self, name, expected_value):
405
+ actual_value = self.get_value(name)
406
+ if actual_value != expected_value:
407
+ raise Exception(f'Got unexpected value for {name}: Expected {expected_value}, got {actual_value}')
408
+
409
+ def assert_is_idle(self):
410
+ run_count, state_change_count, step_info = self.step()
411
+ if run_count != 0:
412
+ raise Exception(f'Is not idle, executed {run_count} procs')
413
+
414
+ def advance_fake_monotonic_time(self, offset):
415
+ if self.fake_monotonic_time_offset is None:
416
+ print('Warning: Setting fake monotonic time offset', flush=True)
417
+ self.fake_monotonic_time_offset = blocks.Duration(duration_ns=0)
418
+ self.fake_monotonic_time_offset += offset
wirely/enum.py ADDED
@@ -0,0 +1,15 @@
1
+ import enum
2
+
3
+
4
+ # TODO: Move enums somewhere else?
5
+ # See Matter Application Cluster Specification
6
+ class ThermostatSystemMode(enum.Enum):
7
+ Off = 0
8
+ Auto = 1
9
+ Cool = 3
10
+ Heat = 4
11
+ EmergencyHeat = 5
12
+ Precooling = 6
13
+ FanOnly = 7
14
+ Dry = 8
15
+ Sleep = 9
wirely/example.py ADDED
@@ -0,0 +1,36 @@
1
+ import asyncio
2
+ import sys
3
+ import os
4
+
5
+ import wirely as blocks
6
+ from wirely.util import check_parameters
7
+
8
+ # TODO: Where should these imports be done?
9
+ import wirely.compiler
10
+ import wirely.run
11
+
12
+ fn = os.path.dirname(__file__) + '/../examples/test.wrl'
13
+ global_block_type = blocks.compiler.compile_files([fn])
14
+ if global_block_type is None:
15
+ sys.exit(1)
16
+
17
+ prog = blocks.Program.compile(global_block_type)
18
+ if prog is None:
19
+ sys.exit(1)
20
+
21
+ eng = blocks.Engine(prog)
22
+
23
+ # eng.debug = True
24
+
25
+ inp1_inst = eng.native_states[(global_block_type.blocks['inp1'],)]
26
+ inp2_inst = eng.native_states[(global_block_type.blocks['inp2'],)]
27
+
28
+ if True:
29
+ inp1_inst.set_value(True)
30
+ inp2_inst.set_value(True)
31
+ eng.step()
32
+ eng.step()
33
+
34
+ inp2_inst.set_value(False)
35
+
36
+ asyncio.run(blocks.run.run(eng))
wirely/graphviz.py ADDED
@@ -0,0 +1,107 @@
1
+ import graphviz
2
+
3
+ def htmlescape(s):
4
+ return s.replace('&', '&amp;').replace('"', '&quot;').replace('<', '&lt;').replace('>', '&gt;')
5
+
6
+ def get_graph(graph, engine=None):
7
+ digraph = graphviz.Digraph()
8
+
9
+ for block_name in graph.blocks:
10
+ block = graph.blocks[block_name]
11
+
12
+ # label = f'{block.name}\n{block.block_type.name}'
13
+
14
+ inputs = ''
15
+ for input in block.all_block_inputs:
16
+ label = input.name
17
+ if label == '':
18
+ label = '()'
19
+ if engine is not None and engine.cache is not None:
20
+ # TODO: Clean up .temporary
21
+ key = (input.temporary.block), input.temporary
22
+ if key in engine.program.variable_map:
23
+ label += f' = {engine.cache[engine.program.variable_map[key]]}'
24
+ else:
25
+ # TODO: Handle this
26
+ label += f' = MISSING'
27
+ inputs += f'<TD PORT="{htmlescape(input.name)}">{htmlescape(label)}</TD>'
28
+ if inputs != '':
29
+ inputs = f'''
30
+ <TR><TD>
31
+ <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
32
+ <TR>{inputs}</TR>
33
+ </TABLE>
34
+ </TD></TR>
35
+ '''
36
+
37
+ outputs = ''
38
+ for output in block.block_outputs:
39
+ label = output.name
40
+ if label == '':
41
+ label = '()'
42
+ if engine is not None and engine.cache is not None:
43
+ # TODO: Clean up .temporary
44
+ key = (output.temporary.block,), output.temporary
45
+ if key in engine.program.variable_map:
46
+ label += f' = {engine.cache[engine.program.variable_map[key]]}'
47
+ else:
48
+ # TODO: Handle this
49
+ label += f' = MISSING'
50
+ outputs += f'<TD PORT="{htmlescape(output.name)}">{htmlescape(label)}</TD>'
51
+ if outputs != '':
52
+ outputs = f'''
53
+ <TR><TD>
54
+ <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
55
+ <TR>{outputs}</TR>
56
+ </TABLE>
57
+ </TD></TR>
58
+ '''
59
+
60
+ s = f'''<
61
+ <TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0">
62
+ {inputs}
63
+ <TR><TD>
64
+ <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
65
+ <TR>
66
+ <TD>{htmlescape(block.name)}</TD>
67
+ </TR>
68
+ <TR>
69
+ <TD>{htmlescape(block.block_type.name.id_str)}</TD>
70
+ </TR>
71
+ </TABLE>
72
+ </TD></TR>
73
+ {outputs}
74
+ </TABLE>
75
+ >'''
76
+ # digraph.node(block.name, label, shape='box')
77
+ digraph.node(block.name, s, shape='plaintext')
78
+
79
+ for block_name in graph.blocks:
80
+ block = graph.blocks[block_name]
81
+ for input in block.all_block_inputs:
82
+ if input.connected_source is not None:
83
+ source = input.connected_source
84
+ source_block = source.block
85
+
86
+ # label = f'{source.name} -> {input.name}'
87
+ # digraph.edge(source_block.name, block.name, label=label)
88
+
89
+ # edge_name = block.name + '/' + input.name
90
+ # digraph.node(edge_name, '', shape='point')
91
+ # digraph.edge(source_block.name, edge_name, label=source.name)
92
+ # digraph.edge(edge_name, block.name, label=input.name)
93
+
94
+ digraph.edge(source_block.name + ':' + source.name, block.name + ':' + input.name)
95
+ digraph.node(block_name)
96
+
97
+ return digraph
98
+
99
+
100
+ def run(args):
101
+ import wirely.compiler
102
+
103
+ graph = wirely.compiler.compile_files([args.input_filename])
104
+ if graph is None:
105
+ return 1
106
+
107
+ get_graph(graph).view()
wirely/knx/__init__.py ADDED
File without changes
@@ -0,0 +1,90 @@
1
+ """Connect xknx TCP tunnels through Wirely-provided stream connections."""
2
+
3
+ import asyncio
4
+
5
+ import xknx
6
+ from xknx.io import ConnectionConfig, ConnectionType, KNXIPInterface, TCPTunnel
7
+ from xknx.io.transport import TCPTransport
8
+
9
+
10
+ class StreamConnectionConfig(ConnectionConfig):
11
+ """TCP tunnel configuration with a callback for opening its stream."""
12
+
13
+ def __init__(self, connect_cb, individual_address=None):
14
+ super().__init__(
15
+ connection_type=ConnectionType.TUNNELING_TCP,
16
+ individual_address=individual_address,
17
+ )
18
+ self.connect_cb = connect_cb
19
+
20
+
21
+ class StreamTCPTransport(TCPTransport):
22
+ """Use xknx's TCP framing over a caller-provided stream."""
23
+
24
+ __slots__ = ("_stream_connect_cb",)
25
+
26
+ def __init__(self, connect_cb, connection_lost_cb):
27
+ super().__init__(
28
+ remote_addr=("0.0.0.0", 0),
29
+ connection_lost_cb=connection_lost_cb,
30
+ )
31
+ self._stream_connect_cb = connect_cb
32
+
33
+ async def connect(self):
34
+ protocol = self.TCPTransportFactory(
35
+ data_received_callback=self.data_received_callback,
36
+ connection_lost_callback=self._connection_lost,
37
+ )
38
+ self.transport, _ = await self._stream_connect_cb(
39
+ asyncio.get_running_loop(), lambda: protocol
40
+ )
41
+
42
+
43
+ class StreamTCPTunnel(TCPTunnel):
44
+ """Create the stream transport while retaining xknx's tunnel logic."""
45
+
46
+ __slots__ = ("_stream_connect_cb",)
47
+
48
+ def __init__(self, xknx_instance, connect_cb, individual_address,
49
+ cemi_received_callback, auto_reconnect, auto_reconnect_wait):
50
+ self._stream_connect_cb = connect_cb
51
+ super().__init__(
52
+ xknx_instance,
53
+ cemi_received_callback=cemi_received_callback,
54
+ gateway_ip="0.0.0.0",
55
+ gateway_port=0,
56
+ individual_address=individual_address,
57
+ auto_reconnect=auto_reconnect,
58
+ auto_reconnect_wait=auto_reconnect_wait,
59
+ )
60
+
61
+ def _init_transport(self):
62
+ self.transport = StreamTCPTransport(
63
+ connect_cb=self._stream_connect_cb,
64
+ connection_lost_cb=self._tunnel_lost,
65
+ )
66
+
67
+
68
+ class StreamKNXIPInterface(KNXIPInterface):
69
+ """Start a TCP tunnel using a StreamConnectionConfig."""
70
+
71
+ async def start(self):
72
+ config = self.connection_config
73
+ self.xknx.cemi_handler.data_secure_init(keyring=None)
74
+ self._interface = StreamTCPTunnel(
75
+ self.xknx,
76
+ connect_cb=config.connect_cb,
77
+ individual_address=config.individual_address,
78
+ cemi_received_callback=self.cemi_received,
79
+ auto_reconnect=config.auto_reconnect,
80
+ auto_reconnect_wait=config.auto_reconnect_wait,
81
+ )
82
+ await self._interface.connect()
83
+
84
+
85
+ def create_xknx(connection_config):
86
+ """Create xknx with the stream interface when the configuration needs one."""
87
+ knx = xknx.XKNX(connection_config=connection_config)
88
+ if isinstance(connection_config, StreamConnectionConfig):
89
+ knx.knxip_interface = StreamKNXIPInterface(knx, connection_config)
90
+ return knx