Home Automation: WAGO

Second part about home automation: my usage of a WAGO, previous chapter: Overview

WAGO: Wiring

Before settling on WAGO I had some crazy ideas to do that with a Raspberry Pi and some custom (low voltage) circuits, but my brother (thanks!) gave me the idea to “just” use such an industrial control system. They are quite expensive, but it is possible to get used ones on ebay for a more affordable price. And my pretty old WAGO is very reliable, I didn’t have a single issue since I installed it.

The WAGO system looks like this:

  • Top green box: Power supply
  • Below on left: The WAGO itself
  • On the right: The extension cards

Revising Lights

With a 24V I/O card, e.g. a 16 channel 750-1504 card it is possible to send a signal as a push button would do it.

Basically:

2 4 V W A G L P P O a u u t s s C c h h h h a i b b n n u u n g t t e t t l s o o w n n x i t 2 c 3 h 0 V L i g h t

But the problem is: A push button can be pressed at any time and then the light will be switch on (or off). The WAGO doesn’t know if the light is currently on or off.

Idea 1: Use a 230V input card to measure if there is currently a voltage on the latching switch. But this idea has several downsides; I think I/O cards are more expensive, but more importantly: I would need to work with high voltage. High voltage is more complicated, dangerous and as someone without an electrical degree, I am simply not allowed to work on it.

Idea 2: Use a dual latching switch. These have two inputs and two outputs; they are independent but are always switched at the same time. Use one input/output pair with 230V to switch to light and the other pair with 24V to signal to the WAGO the current state of the switch. For WAGO there are 24V input cards, e.g. 750-1405.

Now the circuit looks like this:

2 4 V W A G D s P P O u w u u a i s s O l t h h u c t l h b b p a u u u t t t t c t t h o o C i n n h n 2 g 3 0 \ V W A G O I n p u t C h L a i n g n h e t l

What I particularly like about this setup: If the WAGO breaks or is turned off, lighting in the house will continue to work just fine with the push buttons.

Revising Roller Shutters

First idea is to add the WAGO output in parallel to the switches and then the relays will put current on the up or down input of the motor. With just one mechanically locked switch it was easy to make sure that never up and down have a current at the same time, but now the WAGO could send Up and someone presses the Down switch (or is already pressed). Also finding out the current state of shutter (is it up or down) is difficult if there is a bypass. There is no feedback to measure the current position of the roller shutter.

One idea is to re-wire the relays in a way that only one current (either Up or Down) can be active; this should also help against software bugs when I’ll program the WAGO: I might turn on Up and Down in the WAGO at the same time.

R e l 2 a 3 y 0 V U p R e l a y D o w n M M o o t t o o r r U D p o w n

The relays get their input from below and have two outputs: Only the left output has power when the relay is off and only the right output has power when the relay is on (== 24V are on the control input). When just one of the relays is turned on, then there is a current to Up (or Down) of the motor. But if both are turned on, then the Relay Up cuts power to the Relay Down and there is only power on the Up input of the motor.

To be honest, I didn’t try it out if this is enough to protect the motor and I don’t know if this is needed at all.

Another part is: I re-wired the Up/Down switches as an input to the WAGO, and connected output channels to the relays:

S S w w i i t t c c h h U D p o w n W W A A G G O O I I n n p p u u t t c c h h x y W W A A G G O O O O u u t t p p u u t t c c h h a b R R e e l l a a y y U D p o w n

WAGO: I/O cards

The WAGO itself is a tiny, ancient Linux system and the I/O cards are attached to it. I can (with some limitations) plug in any I/O card I like.

Default passwords:

  • web: admin / wago
  • telnet (no ssh): root / wago

I had no prior knowledge about these systems and I barley know enough to achieve what I wanted and I already forgot most of it. So, this part is probably flawed and incomplete.

After attaching I/O cards to the system, they need to be configured in a tool called “WAGO-IO-Check 3”, so the WAGO systems knows what to do with them. There is also some auto-detect which the tool can do, but on my WAGO it needed to be connected via a serial cable just for that. Other tasks can be done via network. Or I can tell it manually which cards I plugged in, which is what I did: I only have a few cards.

My config in “WAGO-IO-Check 3” looks like this:

  • Non-removable card: Connection for power supply
  • Pos 01: 750-430: 8 channel digital input; DC 24V
  • Pos 02: 750-1405: 16 channel digital input; DC 24V
  • Pos 03: 750-1405: 16 channel digital input; DC 24V
  • Pos 04: 750-610: Another connection for power supply; not really needed I think
  • Pos 05: 750-530: 8 channel digital output; DC 24V; 0.5A
  • Pos 06: 750-1504: 16 channel digital output; DC 24V; 0.5A
  • Pos 07: 750-1504: 16 channel digital output; DC 24V; 0.5A
  • Pos 08: 750-600: Termination module, must be present at end of bus

Spare parts: 2x 750-430, 1x 750-1504

WAGO: Programming

Initially I thought: nice Linux system; is there some Python or C interface to work with these i/o cards? Well, no, I had to learn something called “Codesys”. It is at least exotic enough that hugos long list of syntax highlighters doesn’t contain it. I used “ada” which does some coloring.

It is an integrated IDE that shows an ancient user interface and is quite clunky to use, but at least the nice thing is the integrated debugger and direct uploading of the program to my WAGO. Still, I would have prefered a proper programming language. Anyway, the basic idea is: you have a program that checks the state of inputs, sets outputs and then it is repeated in an infinite loop. I had expected more something like: you get events “input x changed to y” and then you react to it.

There are three parts:

  • Main program
  • Inputs and outputs related to light
  • Inputs and outputs related to roller shutters
  • Network communication with OpenHAB

Open codesys IDE:

Main

Main program variables:

1
2
3
4
5
6
PROGRAM PLC_PRG
VAR
    SHUTTER         : ARRAY [1..9] OF SHUTTER_ACTION;
    LIGHT           : ARRAY [1..20] OF LIGHT_ACTION;
    I               : BYTE;
END_VAR
  • Line 3-4: A “LIGHT_ACTION” and a “SHUTTER_ACTION” is a “FUNCTION_BLOCK” which has input and output variables, internal variables and constants. They each have code (see next chapters).

The actual main program related to shutters:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
SHUTTER[1].RUNTIME := 543;
SHUTTER[2].RUNTIME := 543;
SHUTTER[3].RUNTIME := 543;
SHUTTER[4].RUNTIME := 543;
SHUTTER[5].RUNTIME := 300;
SHUTTER[6].RUNTIME := 300;
SHUTTER[7].RUNTIME := 300;
SHUTTER[8].RUNTIME := 300;
SHUTTER[9].RUNTIME := 300;

FOR I:=1 TO 9 BY 1 DO
    SHUTTER[I](NET_SOLL_POS := WORD_TO_BYTE(MBCFG_ModbusSlave.SOLL_POS[I]), NET_COMMAND := WORD_TO_BYTE(MBCFG_ModbusSlave.COMMAND[I]));
END_FOR;

SHUTTER[1](CHANNEL := 0,    STATE := %IX0.0); (* Kueche *)
SHUTTER[1](CHANNEL := 1,    STATE := %IX0.1);
SHUTTER[2](CHANNEL := 0,    STATE := %IX0.2); (* Essen *)
SHUTTER[2](CHANNEL := 1,    STATE := %IX0.3);
SHUTTER[3](CHANNEL := 0,    STATE := %IX0.4); (* Sofa *)
SHUTTER[3](CHANNEL := 1,    STATE := %IX0.5);
SHUTTER[4](CHANNEL := 0,    STATE := %IX0.6); (* Heizraum *)
SHUTTER[4](CHANNEL := 1,    STATE := %IX0.7);
SHUTTER[4](CHANNEL := 2,    STATE := %IX0.8);
SHUTTER[4](CHANNEL := 3,    STATE := %IX0.9);
SHUTTER[5](CHANNEL := 0,    STATE := %IX0.10); (* Kind 1 *)
SHUTTER[5](CHANNEL := 1,    STATE := %IX0.11);
SHUTTER[6](CHANNEL := 0,    STATE := %IX0.12); (* Kind 2 *)
SHUTTER[6](CHANNEL := 1,    STATE := %IX0.13);
SHUTTER[7](CHANNEL := 0,    STATE := %IX0.14); (* Schlafzimmer *)
SHUTTER[7](CHANNEL := 1,    STATE := %IX0.15);
SHUTTER[8](CHANNEL := 0,    STATE := %IX1.0); (* Bad *)
SHUTTER[8](CHANNEL := 1,    STATE := %IX1.1);
SHUTTER[9](CHANNEL := 0,    STATE := %IX1.2); (* HWR *)
SHUTTER[9](CHANNEL := 1,    STATE := %IX1.3);

%QX0.0 := SHUTTER[1].OUTPUT_ACTION = 1;
%QX0.1 := SHUTTER[1].OUTPUT_ACTION = 2;
%QX0.2 := SHUTTER[2].OUTPUT_ACTION = 1;
%QX0.3 := SHUTTER[2].OUTPUT_ACTION = 2;
%QX0.4 := SHUTTER[3].OUTPUT_ACTION = 1;
%QX0.5 := SHUTTER[3].OUTPUT_ACTION = 2;
%QX0.6 := SHUTTER[4].OUTPUT_ACTION = 1;
%QX0.7 := SHUTTER[4].OUTPUT_ACTION = 2;
%QX0.8 := SHUTTER[5].OUTPUT_ACTION = 1;
%QX0.9 := SHUTTER[5].OUTPUT_ACTION = 2;
%QX0.10 := SHUTTER[6].OUTPUT_ACTION = 1;
%QX0.11 := SHUTTER[6].OUTPUT_ACTION = 2;
%QX0.12 := SHUTTER[7].OUTPUT_ACTION = 1;
%QX0.13 := SHUTTER[7].OUTPUT_ACTION = 2;
%QX0.14 := SHUTTER[8].OUTPUT_ACTION = 1;
%QX0.15 := SHUTTER[8].OUTPUT_ACTION = 2;
%QX1.0 := SHUTTER[9].OUTPUT_ACTION = 1;
%QX1.1 := SHUTTER[9].OUTPUT_ACTION = 2;

FOR I:=1 TO 9 BY 1 DO
    MBCFG_ModbusSlave.POSITION[I] := SHUTTER[I].POSITION;
END_FOR;
  • Line 1-9: The “RUNTIME” is an estimate how long it takes (in 100ms) for the shutter to fully open or close, e.g. the first shutter is ~54.3s.
  • Line 11-13: Read from network the target position (0% .. 100%) of the shutter and a NET_COMMAND (will be explained later)
  • Line 15-34: Read the input channels and hand over the state to the shutter subprogram. E.g. “%IX0.0” is the Down switch, “%IX0.1” the Up switch for the first shutter. The STATE is a boolean.
  • Line 23-24: This shutter is special: it has two pairs of switches: one in the machine room, the other in the kitchen
  • Line 36-53: The OUTPUT_ACTION could be 0, then both outputs are off and the shutter is stopped. On 1 only the first output is turned on (down); on 2 the second (up)
  • Line 55-57: Report current estimated position to network

The next lines are the lights:

 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
(* Licht *)
FOR I:=1 TO 20 BY 1 DO
    LIGHT[I](NET_COMMAND := WORD_TO_BYTE(MBCFG_ModbusSlave.COMMAND[I+10]));
END_FOR;

LIGHT[1](STATE := %IX1.4); (* Heizraum *)
LIGHT[2](STATE := %IX1.5); (* Kueche *)
LIGHT[3](STATE := %IX1.6); (* Esstisch *)
LIGHT[4](STATE := %IX1.7); (* Lange Reihe *)
LIGHT[5](STATE := %IX1.8); (* Kurze Reihe *)
LIGHT[6](STATE := %IX1.9); (* Sofa *)
LIGHT[7](STATE := %IX1.10); (* Treppe unten *)
LIGHT[8](STATE := %IX1.11); (* Flur oben *)
LIGHT[9](STATE := %IX1.12); (* Kind 1 *)
LIGHT[10](STATE := %IX1.13); (* Kind 2 *)
LIGHT[11](STATE := %IX1.14); (* Schlafzimmer *)
LIGHT[12](STATE := %IX1.15); (* Flur unten *)
LIGHT[13](STATE := %IX2.0); (* Flur Gaderobe *)
LIGHT[14](STATE := %IX2.1); (* Terrasse *)
LIGHT[15](STATE := %IX2.2); (* Bad unten *)
LIGHT[16](STATE := %IX2.3); (* Ankleide *)
LIGHT[17](STATE := %IX2.4); (* Bad oben  *)
LIGHT[18](STATE := %IX2.5); (* Bad oben Spiegel *)
LIGHT[19](STATE := %IX2.6); (* Treppe oben *)
LIGHT[20](STATE := %IX2.7); (* Dach *)

%QX1.2 := LIGHT[1].OUTPUT_ACTION; (* Heizraum *)
%QX1.3 := LIGHT[2].OUTPUT_ACTION; (* Kueche *)
%QX1.4 := LIGHT[3].OUTPUT_ACTION; (* Esstisch *)
%QX1.5 := LIGHT[4].OUTPUT_ACTION; (* Lange Reihe *)
%QX1.6 := LIGHT[5].OUTPUT_ACTION; (* Kurze Reihe *)
%QX1.7 := LIGHT[6].OUTPUT_ACTION; (* Sofa *)
%QX1.8 := LIGHT[7].OUTPUT_ACTION; (* Treppe unten *)
%QX1.9 := LIGHT[8].OUTPUT_ACTION; (* Flur oben *)
%QX1.10 := LIGHT[9].OUTPUT_ACTION; (* Kind 1 *)
%QX1.11 := LIGHT[10].OUTPUT_ACTION; (* Kind 2 *)
%QX1.12 := LIGHT[11].OUTPUT_ACTION; (* Schlafzimmer *)
%QX1.13 := LIGHT[12].OUTPUT_ACTION; (* Flur unten *)
%QX1.14 := LIGHT[13].OUTPUT_ACTION; (* Flur Gaderobe *)
%QX1.15 := LIGHT[14].OUTPUT_ACTION; (* Terrasse *)
%QX2.0 := LIGHT[15].OUTPUT_ACTION; (* Bad unten *)
%QX2.1 := LIGHT[16].OUTPUT_ACTION; (* Ankleide *)
%QX2.2 := LIGHT[17].OUTPUT_ACTION; (* Bad oben *)
%QX2.3 := LIGHT[18].OUTPUT_ACTION; (* Bad oben Spiegel *)
%QX2.4 := LIGHT[19].OUTPUT_ACTION; (* Treppe oben *)
%QX2.5 := LIGHT[20].OUTPUT_ACTION; (* Dach *)

FOR I:=1 TO 20 BY 1 DO
    IF LIGHT[I].STATE THEN
        MBCFG_ModbusSlave.POSITION[I+10] := 1;
    ELSE
        MBCFG_ModbusSlave.POSITION[I+10] := 0;
    END_IF;
END_FOR;

MBCFG_ModbusSlave();
  • Line 60-62: Read in commands from network
  • Line 64-83: Read current state of a light from i/o card
  • Line 85-104: Enable/disable outputs to turn a light on or off
  • Line 106-112: Report to network
  • Line 114: Drive network (modbus) module

Lights

Next part is the function block “LIGHT_ACTION”. As usual it has a header:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
FUNCTION_BLOCK LIGHT_ACTION
VAR_INPUT
    STATE       : BOOL; (* an oder aus? *)
    NET_COMMAND : BYTE; (* 0: AUS, 1: AN, 2: NOOP (benoetigt, um gleiches kommando nochmal zu akzeptieren) *)
END_VAR
VAR_OUTPUT
    OUTPUT_ACTION : BOOL    := FALSE;
END_VAR
VAR
    LAST_NET_COMMAND    : BYTE := 2;
    IN_PULSE            : BOOL := FALSE;
    PULSE               : TP := (PT := T#100ms);
END_VAR
  • Line 3: The current state (on/off). A state change can happen with a push button (behind WAGOs back).
  • Line 4: Command from network (OpenHAB). 0: light shall be OFF, 1: ON, 2: NOOP.
  • Line 7: State of output channel. To switch the state of the light, this switches to TRUE briefly and then back to simulate a push button.
  • Line 10-12: Internal variables
  • Line 10: Remember last given NET_COMMAND.

Modbus just stores the values that have been written by someone (in our case OpenHAB). But a 0 doesn’t mean: the light shall be OFF forever, because then whenever someone uses a push button, we would immediately turn off the light again. Instead we need to work trigger-based, i.e. when the command changes to 0, then we have to turn off the light. This should also make it clear, why a NOOP is needed: We do not get a notification (or event) that the value has been written, but instead at some point the value is different (or not). Therefore, if OpenHAB would write a 0 again the value doesn’t change and we don’t trigger. So OpenHAB needs to write a 0 wait a moment and write a 2 (NOOP). Next time it wants to turn off the light (because maybe the push button was used and the light is on) it can write a 0 again and we will notice it.

Here is the body:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
IN_PULSE := FALSE;

IF NET_COMMAND >= 0 AND NET_COMMAND <= 2 AND NET_COMMAND <> LAST_NET_COMMAND THEN
    LAST_NET_COMMAND := NET_COMMAND;
    IF NET_COMMAND = 0 THEN
        IF STATE THEN
            (* soll aus sein, ist aber an *)
            IN_PULSE := TRUE;
        END_IF;
    ELSIF NET_COMMAND = 1 THEN
        IF NOT STATE THEN
            (* soll an sein, ist aber aus *)
            IN_PULSE := TRUE;
        END_IF;
    END_IF;
END_IF;

PULSE(IN := IN_PULSE);
OUTPUT_ACTION := PULSE.Q;
  • Line 3: Trigger for NET_COMMAND: It is a valid value and it changed
  • Line 5: Light shall be OFF …
  • Line 6: … but light is ON, therefore send a pulse
  • Line 10-15: Light shall be ON, but is OFF, send pulse
  • Line 18: A built-in pulse block. TP(IN, PT, Q, ET). When IN is TRUE, Q will be TRUE for PT time (ET counts up the time until PT)

Roller Shutters

The roller shutters are more complicated. The inputs are the state of the up or down switch(es) (maybe several!) and the network command.

The ideas I came up with:

  • the last command wins (switch, network)
  • when the down-switch is turned on, then the roller shutter shall go down to 100%. Same for up-switch.
  • when going down(up) and the up(down)-switch turns on, then it shall reverse direction to up(down)
  • if a switch turns from on to off, this means stop
  • the current position shall be estimated (0%..100%)
  • the runtime for 0% to 100% (or back) is just an estimate and calibration is needed: when reaching 0 or 100, keep going for a bit to make sure it always reaches the end position and set position to 0 / 100.

The header of the shutter action function block:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
FUNCTION_BLOCK SHUTTER_ACTION
VAR_INPUT
    RUNTIME      : INT;  (* wie lange braucht der rollladen bis ganz runter/hoch in 100ms? *)
    CHANNEL      : BYTE; (* physische taster *)
    STATE        : BOOL; (* an oder aus? *)
    NET_SOLL_POS : BYTE; (* zielposition 0 bis 100 von modbus / openhab *)
    NET_COMMAND  : BYTE; (* 0: STOP, 1: MOVE auf Zielposition, 2: NOOP (benoetigt, um gleiches kommando nochmal zu akzeptieren) *)
END_VAR
VAR_OUTPUT
    OUTPUT_ACTION : BYTE := 0; (* 0: STOP, 1: DOWN, 2: UP *)
    POSITION      : BYTE := 0; (* 0% .. 100% *)
END_VAR
VAR
    SOLL_POS         : BYTE; (* aktuelle Zielposition *)
    RECALC           : BOOL := FALSE;
    LAST_STATE       : ARRAY [0..3] OF BOOL := FALSE;
    LAST_NET_COMMAND : BYTE := 2;

    TICK             : TON := (PT := T#100ms);
    TICK_COUNT       : INT := 0;
    TICK_COUNT_EXTRA : INT := 0;
END_VAR
VAR CONSTANT
    SOLL_POS_CHANNEL: ARRAY [0..3] OF BYTE := 100, 0,100, 0;
END_VAR
  • Line 3: Static config: time a shutter needs to move from up to down (or vice versa) in 100ms
  • Line 4: Usually two physical switches (Up, Down) are attached to a roller shutter. Once even four.
  • Line 5: Current state of physical switch of given CHANNEL
  • Line 6: Target position of shutter in percent (from network): 0: Up, 50: half closed, 100: down
  • Line 7: Network command: 0: Stop, 1: move to target position, 2: NOOP (see explanation in previous chapter)
  • Line 10: What shall be done, 0: both wago outputs (Up, Down) are OFF, 1: Only down-output is ON, 2: Only up-output is ON
  • Line 11: Current estimate of roller shutter position
  • Line 14-21: A bunch of internal variables
  • Line 14: Current target position. Might be network target from line 6, but also physical switches set targets (0 or 100)
  • Line 19-21: Built-in function block “timer on-delay”. Used to measure time.
  • Line 24: Map physical switch from a channel to a target position. Channel 0: 100% (down), 1: 0% (up), 2: 100% (down), 3: 0% (up)

First part of body:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
IF LAST_STATE[CHANNEL] <> STATE THEN
    LAST_STATE[CHANNEL] := STATE;
    IF STATE THEN
        SOLL_POS := SOLL_POS_CHANNEL[CHANNEL];
        RECALC := TRUE;
    ELSE
        OUTPUT_ACTION := 0; (* STOP *)
    END_IF;
END_IF;

IF NET_COMMAND >= 0 AND NET_COMMAND <= 2 AND NET_COMMAND <> LAST_NET_COMMAND THEN
    LAST_NET_COMMAND := NET_COMMAND;
    IF NET_COMMAND = 0 THEN
        OUTPUT_ACTION := 0; (* STOP *)
    ELSIF NET_COMMAND = 1 THEN
        SOLL_POS := NET_SOLL_POS;
        RECALC := TRUE;
    END_IF;
END_IF;

IF RECALC THEN
    IF SOLL_POS = 0 THEN
        (* POSITION mag ungenau sein, deswegen bei hoch kommando immer erstmal hochfahren *)
        OUTPUT_ACTION := 2; (* UP *)
    ELSIF SOLL_POS = 100 THEN
        (* ebenso *)
        OUTPUT_ACTION := 1; (* DOWN *)
    ELSIF SOLL_POS = POSITION THEN
        (* position scheint schon zu stimmen *)
        OUTPUT_ACTION := 0; (* STOP *)
    ELSIF SOLL_POS < POSITION THEN
        OUTPUT_ACTION := 2; (* UP *)
    ELSE
        OUTPUT_ACTION := 1; (* DOWN *)
    END_IF;
    RECALC := FALSE;
END_IF;
  • Line 1: State of a switch changed
  • Line 2: Remember state
  • Line 4: A up or down switch is now on, set target position to 0 (or 100) as configured in the constant in header line 24
  • Line 7: A switch is now off: stop
  • Line 11: A valid net command was given and it changed
  • Line 13-14: 0==stop
  • Line 15-17: 1==use target position from NET_SOLL_POS
  • Line 21: Command code (for switch and network command)
  • Line 22-27: Keep going at end position to calibrate
  • Line 28: estimate and target match, stop
  • Line 31-34: estimate and target don’t match, go up or down
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
TICK(IN := OUTPUT_ACTION <> 0);

IF TICK.Q THEN
    CASE OUTPUT_ACTION OF
        1: TICK_COUNT := TICK_COUNT + 1;
        2: TICK_COUNT := TICK_COUNT - 1;
    END_CASE;

    IF TICK_COUNT < 0 THEN
        TICK_COUNT := 0;
        TICK_COUNT_EXTRA := TICK_COUNT_EXTRA + 1;
    END_IF;
    IF TICK_COUNT > RUNTIME THEN
        TICK_COUNT := RUNTIME;
        TICK_COUNT_EXTRA := TICK_COUNT_EXTRA + 1;
    END_IF;

    POSITION := INT_TO_BYTE( (TICK_COUNT * 100) / RUNTIME);
    TICK(IN := FALSE);
    TICK(IN := TRUE);

    (* pruefen, ob Zielposition erreicht (oder uebererreicht) ist, dann stoppen *)
    IF OUTPUT_ACTION = 1 THEN (* DOWN *)
        IF SOLL_POS = 100 THEN
            (* nachlaufen lassen, damit wir sicher sind, dass position richtig ist, egal wie sie initialisiert wurde *)
            IF TICK_COUNT_EXTRA > RUNTIME THEN (* POSITION ist schon 100 *)
                OUTPUT_ACTION := 0;
            END_IF;
        ELSE
            IF POSITION >= SOLL_POS THEN
                OUTPUT_ACTION := 0;
            END_IF;
        END_IF;
    ELSIF OUTPUT_ACTION = 2 THEN (* UP *)
        IF SOLL_POS = 0 THEN
            (* nachlaufen lassen, damit wir sicher sind, dass position richtig ist, egal wie sie initialisiert wurde *)
            IF TICK_COUNT_EXTRA > RUNTIME THEN (* POSITION ist schon 0 *)
                OUTPUT_ACTION := 0;
            END_IF;
        ELSE
            IF  POSITION <= SOLL_POS THEN
                OUTPUT_ACTION := 0;
            END_IF;
        END_IF;
    END_IF;
END_IF;

IF OUTPUT_ACTION = 0 THEN
    TICK_COUNT_EXTRA := 0;
END_IF;
  • Line 39: If we are moving, measure the next 100ms, when they have elapsed TICK.Q in line 41 is TRUE. Remember: this program is constantly re-executed; multiple times in 100ms.
  • Line 42-45: TICK_COUNT is increased/decreased by one every 100ms.
  • Line 47-54: When the end position is reached, TICK_COUNT stays at 0/RUNTIME, but TICK_COUNT_EXTRA is further incremented. This is the calibration at work.
  • Line 56: From the current time elapsed we calculate the current position
  • Line 57-58: Start next tick
  • Line 61-67,73-77: The end position is reached, keep going for another RUNTIME and then stop. (Initially some of our roller shutter were half broken, i.e. the motors were stuttering and it took an unpredictable amount of time for a full cycle; now instead of RUNTIME, a smaller constant like 10 (==1s) should be also fine, but it doesn’t hurt)
  • Line 68-70,79-81: Stop if target position (that is not an end position) is reached.
  • Line 86-88: Reset extra ticks when stopped, i.e. at 100% it is possible to stop and go down again for another RUNTIME (extra ticks); quite useful when the shutters were broken and they were actually not at 100% yet.

Network

Initially I was looking for a way to send/receive TCP or UDP messages, but I had to learn that this is not The Way. Modbus is something this thing can do. This is some weird protocol from the serial line world that got a second life in the IP world. Basically it allows you to set and get values (numbers) addressed via some numbers (that have to be in certain ranges). Somewhat similar to SNMP, but a lot less flexible and limited. It probably makes all sense, when looking at the serial world, but in the IP world, it is just a strange beast, for me.

Luckily, OpenHAB speaks modbus. Common ground, yeah. But it still needs some clutches.

I picked these addresses:

  • 31000..31009: current roller shutter position (written by WAGO, read by OpenHAB)
  • 41000..41009: target position (read by WAGO, written by OpenHAB)
  • 41100..41109: network command (0: stop, 1: move to target position, 2: NOOP)
  • (31000, 41000, 41100): first roller shutter
  • (31001, 41001, 41101): second roller shutter
  • 31010..31029: current light state
  • 41110..41129: set light state

The 30000 range is for reading from WAGO, the 40000 range is for writing to WAGO. It seems to be always a 16-bit value. In the IDE this mapping needs to be configured.

The OpenHAB setup to drive this modbus interface correctly is ugly to say the least, but it works. I really hope there is a better way to do all this, but after I got this to work like this, I lost interest in improving it further. Enough pain :)

I’ll explain the OpenHAB part in another note.