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.
- wirely/__init__.py +406 -0
- wirely/__main__.py +45 -0
- wirely/ast.py +1135 -0
- wirely/block.py +542 -0
- wirely/block_type.py +342 -0
- wirely/blocks.lark +126 -0
- wirely/compiler.py +125 -0
- wirely/engine.py +418 -0
- wirely/enum.py +15 -0
- wirely/example.py +36 -0
- wirely/graphviz.py +107 -0
- wirely/knx/__init__.py +0 -0
- wirely/knx/connection_adapter.py +90 -0
- wirely/knx/connection_config.py +152 -0
- wirely/lib/abb_jra.wrl +64 -0
- wirely/lib/csv.wrl +32 -0
- wirely/lib/ext_prog.wrl +12 -0
- wirely/lib/gpio.wrl +23 -0
- wirely/lib/knx.wrl +80 -0
- wirely/lib/memory.wrl +45 -0
- wirely/lib/mqtt.wrl +66 -0
- wirely/lib/std.wrl +134 -0
- wirely/lib/time.wrl +93 -0
- wirely/lib/two_n.wrl +111 -0
- wirely/misc/__init__.py +0 -0
- wirely/misc/service.py +48 -0
- wirely/native/__init__.py +74 -0
- wirely/native/abb_jra.py +66 -0
- wirely/native/csv.py +194 -0
- wirely/native/ext_prog.py +77 -0
- wirely/native/gpio.py +125 -0
- wirely/native/knx.py +455 -0
- wirely/native/memory.py +113 -0
- wirely/native/mqtt.py +605 -0
- wirely/native/std.py +334 -0
- wirely/native/test_library.py +14 -0
- wirely/native/time.py +359 -0
- wirely/native/two_n.py +632 -0
- wirely/node.py +125 -0
- wirely/parser.py +175 -0
- wirely/program.py +155 -0
- wirely/run.py +143 -0
- wirely/service.py +65 -0
- wirely/test.py +569 -0
- wirely/time.py +172 -0
- wirely/two_n/__init__.py +0 -0
- wirely/two_n/cert.py +126 -0
- wirely/two_n/client.py +402 -0
- wirely/two_n/fake_server.py +415 -0
- wirely/two_n/grab_images.py +146 -0
- wirely/types.py +522 -0
- wirely/util.py +111 -0
- wirely/value.py +163 -0
- wirely-0.1.0.dist-info/METADATA +94 -0
- wirely-0.1.0.dist-info/RECORD +59 -0
- wirely-0.1.0.dist-info/WHEEL +5 -0
- wirely-0.1.0.dist-info/entry_points.txt +2 -0
- wirely-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
}
|
wirely/misc/__init__.py
ADDED
|
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
|
wirely/native/abb_jra.py
ADDED
|
@@ -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)
|