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/lib/time.wrl ADDED
@@ -0,0 +1,93 @@
1
+ // TODO: Should this only contain real-time-related blocks or also monotonic-time-related blocks?
2
+
3
+ namespace Time {
4
+ // Block which return false if the current time is before input, true if the current time is at or after input and None if the current time is not comparable to input.
5
+ node type MonotonicTimeReached {
6
+ implicit input : value MonotonicTime;
7
+
8
+ implicit output : value Option[boolean];
9
+
10
+ native procedure run depends on implicit input produces implicit output;
11
+ }
12
+
13
+ // Delay changes until the input has be stable for at least time
14
+ node type DelayChange {
15
+ parameter Type : ValueType;
16
+
17
+ // TODO: Probably should be renamed to Duration
18
+ parameter Time : Duration;
19
+
20
+ implicit input : value Type;
21
+
22
+ implicit output : value Type;
23
+
24
+ // The value which was last sent as output
25
+ state CurrentValue : Option[Type] = None;
26
+ // The value which was the last input
27
+ state NewValue : Option[Type] = None;
28
+ // The time when the output will change to NewValue if there are no further changes
29
+ state UpdateTime : MonotonicTime = MonotonicTime.Invalid;
30
+
31
+ node Reached : MonotonicTimeReached;
32
+ // TODO: Allow this?
33
+ // connect UpdateTime.old -> Reached;
34
+ temporary UpdateTimeOld : MonotonicTime;
35
+ native procedure read_update_time produces UpdateTimeOld;
36
+ connect UpdateTimeOld -> Reached;
37
+
38
+ native procedure run depends on implicit input, Reached produces implicit output, CurrentValue, NewValue, UpdateTime;
39
+ }
40
+
41
+ // Note: Should not be used directly if possible
42
+ native singleton node type TimeOfDayMinute {
43
+ implicit output : value TimeOfDay;
44
+
45
+ // TODO: Should this be "stage input"? Should this use some global time?
46
+ native procedure get_value produces implicit output;
47
+ }
48
+
49
+ // TODO: Implement this in a way which does not cause code to be run every minute
50
+ node type IsBetween {
51
+ // TODO: capitalization of parameters?
52
+ parameter start : TimeOfDay;
53
+ parameter end : TimeOfDay;
54
+
55
+ implicit output : value boolean;
56
+
57
+ node current_time : TimeOfDayMinute;
58
+
59
+ native procedure run depends on current_time produces implicit output;
60
+ }
61
+
62
+ // Creates an event whenever the current time changes from the range of 8 hours
63
+ // before Time to the range 2 hours after Time, if since the last time it was
64
+ // in the range 2 hours after Time the time was at lease once outside both
65
+ // ranges.
66
+ // 8/2 hours should be long enough to allow triggering even during DST changes
67
+ // and make sure that it is triggered only once a day during DST changes.
68
+ // TODO: Make ranges configurable? (In that case something needs to make sure that the don't overlap, and probably that they don't add up to 24h.)
69
+ node type TriggerAt {
70
+ parameter Time : TimeOfDay;
71
+
72
+ // TODO: Should this be called Type/Value or OutputType/OutputValue?
73
+ // Defaults to unit
74
+ parameter Type : ValueType = unit;
75
+ // Defaults to ()
76
+ optional parameter Value : Type;
77
+
78
+ implicit output : event Type;
79
+
80
+ // Keeps track of the last state
81
+ // TODO: Introduce an enum for this?
82
+ // 0: Initial state / outside any range
83
+ // 1: In 'before' range, transition to 'after' range will trigger output
84
+ // 2: Has already triggered (or at least was in 'after' range), waiting for time outside any range
85
+ state State : uint8 = 0;
86
+
87
+ // Note: This relies on both blocks using the same time source, so that there is no cycle/step at the Time where both or neither are true.
88
+ node before : IsBetween(start = TimeOfDay_AddWrap(Time, "-08:00"), end = Time);
89
+ node after : IsBetween(start = Time, end = TimeOfDay_AddWrap(Time, "02:00"));
90
+
91
+ native procedure run depends on before, after produces implicit output, State;
92
+ }
93
+ }
wirely/lib/two_n.wrl ADDED
@@ -0,0 +1,111 @@
1
+ namespace TwoN {
2
+ native node type Device {
3
+ parameter Hostname : String;
4
+ parameter Port : uint16 = 443;
5
+ parameter TLSA : List[String];
6
+ parameter UsernamePasswordFile : String;
7
+
8
+ implicit output : two_n_device;
9
+ }
10
+
11
+ // Get an event whenever a key is pressed or released
12
+ native node type KeyEvent {
13
+ parameter Key : String;
14
+
15
+ input Device : two_n_device;
16
+
17
+ // True if the key is pressed, false if it released.
18
+ implicit output : event boolean;
19
+
20
+ native procedure get_value stage input produces implicit output;
21
+ }
22
+
23
+ // Get the current state of a key
24
+ //
25
+ // Might be incorrect after startup if key is already pressed on startup.
26
+ // TODO: When the connection is lost, this also should reset the value to "False"
27
+ node type KeyState {
28
+ parameter Key : String;
29
+ // Timeout after which the key will be assumed to be released (because
30
+ // "2N -> Calling -> Dialing -> Quick dial button functionality test" will
31
+ // trigger a KeyPressed event but no KeyReleased event).
32
+ parameter Timeout : Duration = "30s";
33
+
34
+ input Device : two_n_device;
35
+
36
+ // True if the key is pressed, false otherwise.
37
+ implicit output : value boolean;
38
+
39
+ node key_event : KeyEvent(Key=Key);
40
+ connect Device -> key_event.Device;
41
+
42
+ // The current value which is kept if there is no new event
43
+ state CurrentValue : boolean = false;
44
+
45
+ // The next time at which the the state will be reset to False.
46
+ // If this time is not comparable to the current time, the the state will be reset to False.
47
+ state TimeoutTime : MonotonicTime = MonotonicTime.Invalid;
48
+
49
+ node TimeoutReached : Time.MonotonicTimeReached;
50
+ // TODO: Allow this?
51
+ // connect TimeoutTime.old -> TimeoutReached;
52
+ temporary TimeoutTimeOld : MonotonicTime;
53
+ native procedure read_timeout_time produces TimeoutTimeOld;
54
+ connect TimeoutTimeOld -> TimeoutReached;
55
+
56
+ native procedure get_value depends on key_event, TimeoutReached produces implicit output, TimeoutTime, CurrentValue;
57
+ }
58
+
59
+ // Get the current state of an input or output
60
+ // TODO: Regularly get the current value?
61
+ native node type IOState {
62
+ parameter Port : String;
63
+
64
+ input Device : two_n_device;
65
+
66
+ // True if the input/output is on, false otherwise.
67
+ implicit output : value boolean;
68
+
69
+ native procedure get_value stage input produces implicit output;
70
+ }
71
+
72
+ // Get the current state of a switch
73
+ // TODO: Regularly get the current value?
74
+ native node type SwitchState {
75
+ parameter SwitchNumber : int32;
76
+
77
+ input Device : two_n_device;
78
+
79
+ // True if the switch is active, false otherwise.
80
+ implicit output : value boolean;
81
+
82
+ // TODO: Outputs for "locked" and "held"?
83
+
84
+ native procedure get_value stage input produces implicit output;
85
+ }
86
+
87
+ native node type CodeEnteredEvent {
88
+ parameter Code : String;
89
+
90
+ input Device : two_n_device;
91
+
92
+ implicit output : event unit;
93
+
94
+ native procedure get_value stage input produces implicit output;
95
+ }
96
+
97
+ native node type OutputControl {
98
+ parameter Port : String;
99
+
100
+ input Device : two_n_device;
101
+
102
+ // True to turn the output is on, false to turn it off.
103
+ implicit input : event boolean;
104
+
105
+ native procedure send_value stage output depends on implicit input;
106
+ }
107
+
108
+ // TODO: Block for setting output from value?
109
+
110
+ // TODO: Block for setting switch from event? Also allow toggle? Also allow lock/unlock/hold/release?
111
+ }
File without changes
wirely/misc/service.py ADDED
@@ -0,0 +1,48 @@
1
+ import asyncio
2
+ import sys
3
+ import signal
4
+
5
+
6
+ # TODO: Should this be in service.util?
7
+
8
+ # TODO: Shouldn't the handlers be set earlier for most callers?
9
+ # Maybe this should be split into a part which throws when a signal is received and a part which just sleeps until canceled
10
+ async def loop_until_sigint(allow_sigterm=True) -> None:
11
+ """Loop until Crtl-C was pressed."""
12
+
13
+ sigint_received = asyncio.Event()
14
+
15
+ def sigint_handler() -> None:
16
+ """End loop."""
17
+ sigint_received.set()
18
+
19
+ if sys.platform == "win32":
20
+ print("Windows does not support signals", flush=True)
21
+ else:
22
+ loop = asyncio.get_running_loop()
23
+ loop.add_signal_handler(signal.SIGINT, sigint_handler)
24
+ if allow_sigterm:
25
+ loop.add_signal_handler(signal.SIGTERM, sigint_handler)
26
+ print("Press Ctrl+C to stop", flush=True)
27
+ await sigint_received.wait()
28
+ # TODO: Reset handlers?
29
+
30
+
31
+ class TerminateProgram(BaseException):
32
+ pass
33
+
34
+
35
+ async def loop_until_sigint_and_throw(allow_sigterm=True):
36
+ await loop_until_sigint(allow_sigterm=allow_sigterm)
37
+ # Raising a KeyboardInterrupt (or SystemExit) here causes a
38
+ # "Task exception was never retrieved" message
39
+ # See <https://github.com/python/cpython/issues/101515>
40
+ # Raise a different exception instead
41
+ # raise KeyboardInterrupt()
42
+ raise TerminateProgram()
43
+
44
+
45
+ async def should_never_return(awaitable):
46
+ # TODO: Get a stacktrace here and show that if awaitable returns?
47
+ await awaitable
48
+ raise Exception('Awaitable which should never return has returned: {}'.format(awaitable))
@@ -0,0 +1,74 @@
1
+ import inspect
2
+ import importlib
3
+ import typing
4
+
5
+ import wirely as blocks
6
+ from wirely.util import check_parameters
7
+
8
+
9
+ @check_parameters
10
+ def native_block_type(namespace_dict, *, has_state: bool, data_type: typing.Optional[type] = None):
11
+ def native_block_type_impl(cls):
12
+ cls_name = cls.__name__
13
+
14
+ cls.native_block_has_state = has_state
15
+ cls.native_block_data_type = data_type
16
+
17
+ native_procedures = {}
18
+ for name, value in cls.__dict__.items():
19
+ if not inspect.isfunction(value):
20
+ continue
21
+ if not hasattr(value, 'is_native_procedure') or not value.is_native_procedure:
22
+ continue
23
+
24
+ if not has_state and not isinstance(inspect.getattr_static(cls, name), staticmethod):
25
+ raise Exception(f'Native procedure name {name!r} in block {cls_name!r} is not static but block type is stateless')
26
+
27
+ if name in native_procedures:
28
+ raise Exception(f'Duplicate native procedure name {name!r} in block {cls_name!r}')
29
+ native_procedures[name] = value
30
+ cls.native_procedures = native_procedures
31
+
32
+ if cls_name in namespace_dict:
33
+ raise Exception(f'Duplicate native block name {cls_name!r}')
34
+ namespace_dict[cls_name] = cls
35
+
36
+ return cls
37
+ return native_block_type_impl
38
+
39
+
40
+ def native_procedure(proc):
41
+ # TODO: Check parameters of procedure?
42
+ proc.is_native_procedure = True
43
+ return proc
44
+
45
+
46
+ native_block_pkgs = {
47
+ ('Std',): 'std',
48
+ ('Time',): 'time',
49
+ ('KNX',): 'knx',
50
+ ('ABB', 'JRA'): 'abb_jra',
51
+ ('MQTT',): 'mqtt',
52
+ ('CSV',): 'csv',
53
+ ('TwoN',): 'two_n',
54
+ ('GPIO',): 'gpio',
55
+ ('TestLibrary',): 'test_library',
56
+ ('Memory',): 'memory',
57
+ ('ExtProg',): 'ext_prog',
58
+ }
59
+
60
+
61
+ @check_parameters
62
+ def get_native_block_type(block_type: blocks.BlockType):
63
+ pkg_name = native_block_pkgs.get(block_type.fullname_tuple[:-1], None)
64
+ if pkg_name is None:
65
+ raise Exception(f'Block type {block_type.fullname!r} needs a native class but no native classes found for {block_type.fullname_tuple[:-1]}')
66
+
67
+ pkg = importlib.import_module('.' + pkg_name, __name__)
68
+ native_classes = pkg.native_block_types
69
+
70
+ native_class = native_classes.get(block_type.name.id_str, None)
71
+ if native_class is None:
72
+ raise Exception(f'Block type {block_type.fullname!r} needs a native class but cannot find one')
73
+
74
+ return native_class
@@ -0,0 +1,66 @@
1
+ import enum
2
+
3
+ import wirely as blocks
4
+ from wirely.native import native_block_type, native_procedure
5
+
6
+ native_block_types = {}
7
+
8
+
9
+ class JRAStatus(enum.IntFlag):
10
+ # See JRAS_xxx1_PH_EN_V1-4_2CDC506051D0204.pdf, page 106
11
+
12
+ # Low byte
13
+ ManualOperation = (1 << 0)
14
+ BlockActive = (1 << 1)
15
+ ForcedOperation = (1 << 2)
16
+ FrostAlarm = (1 << 3)
17
+ RainAlarm = (1 << 4)
18
+ WindAlarm = (1 << 5)
19
+ AutomaticSunProtection = (1 << 6)
20
+ AutomaticHeatingCooling = (1 << 7)
21
+
22
+ # High byte
23
+ DriveError = (1 << 8)
24
+ DriveInMotion = (1 << 9)
25
+ DriveDown = (1 << 10)
26
+ DriveUp = (1 << 11)
27
+ SendReceiveDelayActive = (1 << 12)
28
+ # Unassigned13 = (1 << 13)
29
+ # Unassigned14 = (1 << 14)
30
+ # Unassigned15 = (1 << 15)
31
+
32
+ @native_block_type(native_block_types, has_state=False)
33
+ class InterpretStatusInformation:
34
+ @native_procedure
35
+ @staticmethod
36
+ def run(*, context):
37
+ input_var = context.static.sources['implicit input']
38
+
39
+ val = context.dep_values[input_var]
40
+ val = JRAStatus(val)
41
+
42
+ for flag in JRAStatus:
43
+ name = flag.name
44
+ output_var = context.static.targets[name]
45
+ res = bool(val & flag)
46
+ context.results[output_var] = res
47
+
48
+ @native_block_type(native_block_types, has_state=False)
49
+ class InterpretStatusInformationOption:
50
+ @native_procedure
51
+ @staticmethod
52
+ def run(*, context):
53
+ input_var = context.static.sources['implicit input']
54
+
55
+ val = context.dep_values[input_var]
56
+ if val is not None:
57
+ val = JRAStatus(val[0])
58
+
59
+ for flag in JRAStatus:
60
+ name = flag.name
61
+ output_var = context.static.targets[name]
62
+ if val is None:
63
+ res = None
64
+ else:
65
+ res = (bool(val & flag),)
66
+ context.results[output_var] = res
wirely/native/csv.py ADDED
@@ -0,0 +1,194 @@
1
+ import asyncio
2
+ import contextlib
3
+ import dataclasses
4
+ import os
5
+ import sys
6
+
7
+ import wirely as blocks
8
+ from wirely.native import native_block_type, native_procedure
9
+
10
+ native_block_types = {}
11
+
12
+
13
+ class CsvValue:
14
+ def __init__(self, value, do_quote):
15
+ self.value = value
16
+ self.do_quote = do_quote
17
+
18
+ if not do_quote:
19
+ if '\r' in value or '\n' in value or ',' in value or '"' in value:
20
+ raise Exception(f'Attempting to create CsvValue for string {value!r} without quoting')
21
+
22
+
23
+ class MyCsvWriter:
24
+ def __init__(self, stream):
25
+ self.stream = stream
26
+
27
+ # https://www.rfc-editor.org/rfc/rfc4180
28
+ def write_row(self, row):
29
+ csv_string = ''
30
+ for i, value in enumerate(row):
31
+ if not isinstance(value, CsvValue):
32
+ raise Exception(f'Expected a CsvValue, got a {type(value)}')
33
+
34
+ if i > 0:
35
+ csv_string += ','
36
+
37
+ if value.do_quote:
38
+ csv_string += '"' + value.value.replace('"', '""')
39
+ else:
40
+ csv_string += value.value
41
+ csv_string += '\r\n'
42
+
43
+ encoded = csv_string.encode('utf-8')
44
+ # print(encoded)
45
+ # TODO: Use os.write or stream.write? Is stream.write atomic?
46
+ self.stream.write(encoded)
47
+
48
+
49
+ @dataclasses.dataclass(frozen=True)
50
+ class CsvWriterParameters:
51
+ type: blocks.Type
52
+ filename: str
53
+
54
+ @staticmethod
55
+ def create(*, parameter_values):
56
+ ty = parameter_values['Type']
57
+ if not ty.has_to_csv_value:
58
+ # TODO: Error message position?
59
+ raise Exception(f'Type {ty} does not support converting to CSV')
60
+
61
+ filename = parameter_values['Filename'].value
62
+
63
+ return CsvWriterParameters(type=ty, filename=filename)
64
+
65
+
66
+ @native_block_type(native_block_types, has_state=True, data_type=CsvWriterParameters)
67
+ class ValueWriter(blocks.NativeState):
68
+ # TODO: Should writing to the file be done on a background thread?
69
+
70
+ def __init__(self, path, engine, static_block_context):
71
+ super().__init__(path, engine, static_block_context)
72
+
73
+ self.file = None
74
+
75
+ self.data = static_block_context.parsed_native_instance
76
+
77
+ self.last_value_csv = ('', False)
78
+
79
+ # TODO: Clean this up
80
+ proc = None
81
+ for p in self.block_or_singleton.block_procedures:
82
+ if p.name == 'run':
83
+ proc = p
84
+ if proc is None:
85
+ raise Exception('proc is None')
86
+ self.proc = proc
87
+
88
+ def start_tasks(self, task_group, async_exit_stack: contextlib.AsyncExitStack):
89
+ super().start_tasks(task_group, async_exit_stack)
90
+
91
+ async_exit_stack.enter_context(self.open_file())
92
+
93
+ @contextlib.contextmanager
94
+ def open_file(self):
95
+ if self.file is not None:
96
+ raise Exception('File already open')
97
+
98
+ print(f'Opening CSV file {self.data.filename!r}', flush=True)
99
+ with open(self.data.filename, 'a+b', buffering=0) as file:
100
+ size = file.seek(0, 2)
101
+ if size != 0:
102
+ last_byte = os.pread(file.fileno(), 1, size - 1)
103
+ print(f' got byte at end: {last_byte!r}', flush=True)
104
+ if last_byte == b'':
105
+ raise Exception('Failed to read last byte')
106
+ elif last_byte != b'\n':
107
+ print(f' Incomplete line at end, writing newline', flush=True)
108
+ # Note: Write only \n instead of \r\n, so a reader can see that this was an incomplete line
109
+ # TODO: Use os.write instead?
110
+ file.write(b'\n')
111
+
112
+ try:
113
+ self.file = file
114
+ self.writer = MyCsvWriter(file)
115
+
116
+ try:
117
+ yield
118
+ finally:
119
+ time = self.engine.shutdown_time
120
+ if time is None:
121
+ if self.last_value_csv != ('', False):
122
+ print(f'CSV.ValueWriter: Warning: engine.shutdown_time is None during shutdown but a value has been written', flush=True, file=sys.stderr)
123
+ else:
124
+ self.write_value(('', False), time)
125
+ # Make sure that if after the shutdown another step is run run() will be called again
126
+ self.engine.trigger(self.block_path, self.proc)
127
+ finally:
128
+ self.writer = None
129
+ self.file = None
130
+
131
+ # TODO: Periodically re-write the current value
132
+
133
+ def write_value(self, new_value_csv, time, *, force=False):
134
+ if self.last_value_csv == new_value_csv and not force:
135
+ return False
136
+
137
+ row = [
138
+ CsvValue(time.isoformat(), False),
139
+ CsvValue(*self.last_value_csv),
140
+ CsvValue(*new_value_csv),
141
+ ]
142
+ # print(f'CSV: Got value {new_value} {time.isoformat()}')
143
+ if self.writer is None:
144
+ # TODO: What should be done here? Seems to happen during shutdown (called from run())
145
+ print(f'CSV.ValueWriter: Warning: self.writer is None in write_value()', flush=True, file=sys.stderr)
146
+ # TODO: What should be returned here?
147
+ return True
148
+ self.writer.write_row(row)
149
+ self.last_value_csv = new_value_csv
150
+ return True
151
+
152
+ @native_procedure
153
+ def read_update_time(self, *, context):
154
+ var_output = context.static.targets['RepeatTimeOld']
155
+ var_state = context.static.state['RepeatTime']
156
+
157
+ value = context.state_old[var_state]
158
+ context.results[var_output] = value
159
+
160
+ @native_procedure
161
+ def run(self, *, context):
162
+ data = context.static.block.parsed_native_instance
163
+
164
+ var_input = context.static.sources['implicit input']
165
+ var_repeat_time_reached = context.static.sources['RepeatTimeReached']
166
+ var_repeat_time = context.static.state['RepeatTime']
167
+
168
+ new_value = context.dep_values[var_input]
169
+ reached = context.dep_values[var_repeat_time_reached]
170
+ repeat_time = context.state_old[var_repeat_time]
171
+
172
+ new_value_csv = self.data.type.to_csv_value(new_value)
173
+
174
+ # If reached is None or (True,), the value has to be repeated
175
+ # Note: The empty value is not repeated
176
+ force = reached != (False,) and new_value_csv != ('', False)
177
+ # print(f'NewValue={new_value} Reached={reached} force={force}')
178
+
179
+ # Note: This uses RunStage.Default to be as consistent as possible with the time seen by procedures in the default stage
180
+ time = context.step_info.stage_start_time[blocks.RunStage.Default]
181
+
182
+ if force:
183
+ # Make sure that if in the next cycle/step the timeout is reached again, the procedure will be called again.
184
+ # TODO: Should this simply be done whenever write_value() is called?
185
+ self.engine.trigger(self.block_path, self.proc)
186
+
187
+ written = self.write_value(new_value_csv, time, force=force)
188
+ if written:
189
+ # TODO: Think what happens if new_value_csv is ('', False)
190
+ now = context.step_info.stage_start_time_monotonic[blocks.RunStage.Default]
191
+ # Write value once an hour
192
+ repeat_time = now + blocks.Duration.parse('PT1H')
193
+
194
+ context.state_new[var_repeat_time] = repeat_time
@@ -0,0 +1,77 @@
1
+ import dataclasses
2
+ import contextlib
3
+ import asyncio
4
+
5
+ import wirely as blocks
6
+ from wirely.native import native_block_type, native_procedure
7
+
8
+ native_block_types = {}
9
+
10
+
11
+ @dataclasses.dataclass(frozen=True)
12
+ class LaunchOnEventData:
13
+ shell_command: str
14
+
15
+ @staticmethod
16
+ def create(*, parameter_values):
17
+ par_shell_command = parameter_values['ShellCommand'].value
18
+
19
+ return LaunchOnEventData(shell_command=par_shell_command)
20
+
21
+
22
+ @native_block_type(native_block_types, has_state=True, data_type=LaunchOnEventData)
23
+ class LaunchOnEvent(blocks.NativeState):
24
+ def __init__(self, path, engine, static_block_context):
25
+ super().__init__(path, engine, static_block_context)
26
+
27
+ self.data = static_block_context.parsed_native_instance
28
+
29
+ self.task = None
30
+ self.pending = False
31
+ self.event = asyncio.Event()
32
+
33
+ # TODO: Clean this up
34
+ proc = None
35
+ for p in self.block_or_singleton.block_procedures:
36
+ if p.name == 'run':
37
+ proc = p
38
+ if proc is None:
39
+ raise Exception('proc is None')
40
+ self.proc = proc
41
+
42
+ def start_tasks(self, task_group, async_exit_stack: contextlib.AsyncExitStack):
43
+ super().start_tasks(task_group, async_exit_stack)
44
+
45
+ if self.task is not None:
46
+ raise Exception('self.task is not None')
47
+
48
+ self.task = self.start_task(task_group, async_exit_stack, self.run_task())
49
+
50
+ @native_procedure
51
+ def run(self, *, context):
52
+ var_input = context.static.sources['implicit input']
53
+ inp = context.dep_values[var_input]
54
+
55
+ if inp is None:
56
+ return
57
+
58
+ if self.pending:
59
+ print('Warning: ExtProg.LaunchOnEvent: Got new trigger while launch is already pending', flush=True)
60
+ self.pending = True
61
+ self.event.set()
62
+
63
+ # This makes sure that the value is sent again if the input does not change
64
+ self.engine.trigger(self.block_path, self.proc)
65
+
66
+ async def run_task(self):
67
+ while True:
68
+ while not self.pending:
69
+ # Note: This relies on the fact that everything is running on one thread and therefore everything without await is atomic with regard to other tasks
70
+ self.event.clear()
71
+ await self.event.wait()
72
+ self.pending = False
73
+
74
+ print(f'Launching command {self.data.shell_command!r}', flush=True)
75
+ proc = await asyncio.create_subprocess_exec('/bin/sh', '-c', self.data.shell_command)
76
+ code = await proc.wait()
77
+ print(f'Command {self.data.shell_command!r} finished with exit code {code}', flush=True)