28 | | == Virtual Device == |
29 | | |
30 | | This will be the central feature of '''pKNyX''', allowing user to create virtual devices which mimics real KNX devices. They will follow the same architecture, like having '''Datapoints''' (aka as Communication Objects), which have to be linked to '''Group Address''' to communicate. |
31 | | |
32 | | Here is a very simple example of what I have in mind: creating a virtual weather station, which uses informations from a non-KNX real weather-station, or from a web site: |
33 | | |
34 | | {{{ |
35 | | #!python |
36 | | from pknyx.api import Device, Stack, ETS |
37 | | |
38 | | stack = Stack() |
39 | | ets = ETS(stack) |
40 | | |
41 | | # Weather station class definition |
42 | | class WeatherStation(Device): |
43 | | |
44 | | # Datapoints (= Group Objects) definition |
45 | | DP_01 = dict(name="temperature", dptId="9.001", flags="CRT", priority="low", initValue=0.) |
46 | | DP_02 = dict(name="humidity", dptId="9.007", flags="CRT", priority="low", initValue=0.) |
47 | | DP_03 = dict(name="wind_speed", dptId="9.005", flags="CRT", priority="low", initValue=0.) |
48 | | DP_04 = dict(name="wind_alarm", dptId="1.005", flags="CRT", priority="urgent", initValue="No alarm") |
49 | | DP_05 = dict(name="wind_speed_limit", dptId="9.005", flags="CWTU", priority="low", initValue=15.) |
50 | | DP_06 = dict(name="wind_alarm_enable", dptId="1.003", flags="CWTU", priority="low", initValue="Disable") |
51 | | |
52 | | # Instanciation of the weather station device object |
53 | | station = WeatherStation(name="weather_station", desc="My simple weather station example", address="1.2.3") |
54 | | |
55 | | # Linking weather station Datapoints to Group Address |
56 | | ets.link(dev=station, dp="temperature", gad="1/1/1") |
57 | | ets.link(dev=station, dp="humidity", gad="1/1/2") |
58 | | ets.link(dev=station, dp="wind_speed", gad="1/1/3") |
59 | | ets.link(dev=station, dp="wind_alarm", gad="1/1/4") |
60 | | ets.link(dev=station, dp="wind_speed_limit", gad="1/1/5") |
61 | | ets.link(dev=station, dp="wind_alarm_enable", gad="1/1/6") |
62 | | }}} |
63 | | |
64 | | That's it! As you can see, concepts used here are not new... This device can be then used from any other real device of your installation, through GADs {{{1/1/1}}} to {{{1/1/6}}}. All you have to do, is to link the Communication Objects of your real devices to these GADs, using the '''ETS''' application. For example, the DP Nr 4 will send the {{{"Alarm"}}} value over the bus when the wind speed will reach the value stored in DP Nr 5, but only if the value of the DP Nr 6 as been set to {{{"Enable"}}}. By linking this alram object to the alarm entry of you sun blinds, you can save them from destruction! |
65 | | |
66 | | Ok, but how the device knows the current wind speed? Well, it does not... yet! Let's see how it can be done: |
67 | | |
68 | | {{{ |
69 | | #!python |
70 | | from pknyx.api import Device, Stack, ETS |
71 | | |
72 | | stack = Stack() |
73 | | ets = ETS(stack) |
74 | | |
75 | | # Weather station class definition |
76 | | class WeatherStation(Device): |
77 | | |
78 | | # Datapoints (= Group Objects) definition |
79 | | DP_01 = dict(name="temperature", dptId="9.001", flags="CRT", priority="low", initValue=0.) |
80 | | DP_02 = dict(name="humidity", dptId="9.007", flags="CRT", priority="low", initValue=0.) |
81 | | DP_03 = dict(name="wind_speed", dptId="9.005", flags="CRT", priority="low", initValue=0.) |
82 | | DP_04 = dict(name="wind_alarm", dptId="1.005", flags="CRT", priority="urgent", initValue="No alarm") |
83 | | DP_05 = dict(name="wind_speed_limit", dptId="9.005", flags="CWTU", priority="low", initValue=15.) |
84 | | DP_06 = dict(name="wind_alarm_enable", dptId="1.003", flags="CWTU", priority="low", initValue="Disable") |
85 | | |
86 | | @Device.schedule.every(minute=5) |
87 | | def checkWindSpeed(self): |
88 | | |
89 | | # How we retreive the speed is out of the scope of this proposal |
90 | | # speed = xxx |
91 | | |
92 | | # Now, write the new speed value to the Datapoint |
93 | | self.dp["wind_speed"].value = speed |
94 | | |
95 | | # Check alarm speed |
96 | | if self.dp["wind_alarm_enable"].value == "Enable": |
97 | | if speed >= self.dp["wind_speed_limit"].value: |
98 | | self.dp["wind_alarm"].value = "Alarm" |
99 | | elif speed < self.dp["wind_speed_limit"].value - 5.: |
100 | | self.dp["wind_alarm"].value = "No alarm" |
101 | | |
102 | | # Instanciation of the weather station device object |
103 | | station = WeatherStation(name="weather_station", desc="My simple weather station example", address="1.2.3") |
104 | | |
105 | | # Linking weather station Datapoints to Group Address |
106 | | ets.link(dev=station, dp="temperature", gad="1/1/1") |
107 | | ets.link(dev=station, dp="humidity", gad="1/1/2") |
108 | | ets.link(dev=station, dp="wind_speed", gad="1/1/3") |
109 | | ets.link(dev=station, dp="wind_alarm", gad="1/1/4") |
110 | | ets.link(dev=station, dp="wind_speed_limit", gad="1/1/5") |
111 | | ets.link(dev=station, dp="wind_alarm_enable", gad="1/1/6") |
112 | | }}} |
113 | | |
114 | | All we do, here, is adding a method periodically called by the framework (every 5 minutes in the above example). In this method, we retreive the wind speed (not explained here), and give the value to the Datapoint Nr 3. If the value has changed from the previous call, the Datapoint will automagically transmit it to the bus (according to the flags, of course). |
115 | | |
116 | | We also check if the speed has reached the speed limit; if so, we change the corresponding Datapoint value, which wil, in turn, send the value on the bus if needed (a special value of the flag, like in '''linknx''', can force the value to be sent, even if it does not change). |
117 | | |
118 | | A last thing: is will be possible to compute and print the '''map table''': |
119 | | |
120 | | {{{ |
121 | | #!python |
122 | | |
123 | | print ets.computeMapTable() |
124 | | |
125 | | {'byDP': {'humidity (station)': ['1/1/2'], |
126 | | 'temperature (station)': ['1/1/1'], |
127 | | 'wind_alarm (station)': ['1/1/4'], |
128 | | 'wind_alarm_enable (station)': ['1/1/6'], |
129 | | 'wind_speed (station)': ['1/1/3'], |
130 | | 'wind_speed_limit (station)': ['1/1/5']}, |
131 | | 'byGAD': {'1/1/1': ['temperature (station)'], |
132 | | '1/1/2': ['humidity (station)'], |
133 | | '1/1/3': ['wind_speed (station)'], |
134 | | '1/1/4': ['wind_alarm (station)'], |
135 | | '1/1/5': ['wind_speed_limit (station)'], |
136 | | '1/1/6': ['wind_alarm_enable (station)']}} |
137 | | }}} |
| 28 | == Functional Blocks == |
| 29 | |
| 30 | This will be the central feature of '''pKNyX''', allowing user to create virtual devices which mimics real KNX devices. The Device itself will be implemented as the process level. |
| 31 | |
| 32 | Here is a very simple example of what I have in mind: creating a virtual minimalistic weather station, which uses informations from a non-KNX real weather-station, or from a web site. This station only implements temperature/humidity. |
| 33 | |
| 34 | {{{ |
| 35 | #!python |
| 36 | from pknyx.api import FunctionalBlock, Stack, ETS |
| 37 | |
| 38 | |
| 39 | class WeatherTemperatureBlock(FunctionalBlock): |
| 40 | |
| 41 | DP_01 = dict(name="temperature", access="output", dptId="9.001", default=19.) |
| 42 | DP_02 = dict(name="humidity", access="output", dptId="9.007", default=50.) |
| 43 | |
| 44 | GO_01 = dict(dp="temperature", flags="CRT", priority="low") |
| 45 | GO_02 = dict(dp="humidity", flags="CRT", priority="low") |
| 46 | |
| 47 | |
| 48 | weatherTempBlock = WeatherTemperatureBlock(name="weather_tempertature", desc="A simple weather block example") |
| 49 | |
| 50 | stack = Stack(individualAddress="1.2.3") |
| 51 | ets = ETS(stack) |
| 52 | |
| 53 | ets.weave(fb=weatherTempBlock, dp="temperature", gad="1/1/1") |
| 54 | ets.weave(fb=weatherTempBlock, dp="humidity", gad="1/1/2") |
| 55 | |
| 56 | stack.run() |
| 57 | }}} |
| 58 | |
| 59 | That's it! As you can see, concepts used here are simple... This Functional Block can be then used from any other real device of your installation, through Groups Addresses {{{1/1/1}}} and {{{1/1/2}}}. All you have to do, is to weave (link, bind...) the Group Objects of your real devices to these Groups Addresses, using the real '''ETS''' application. |
| 60 | |
| 61 | Lets have a closer look to this example. First, we import some python objects: |
| 62 | |
| 63 | {{{ |
| 64 | #!python |
| 65 | from pknyx.api import FunctionalBlock, Stack, ETS |
| 66 | }}} |
| 67 | |
| 68 | These objects are classes. |
| 69 | |
| 70 | Then, we create a custom Functional Block; this is done by subclassing the '''!FunctionBlock''' base class, and adding a few attributes: |
| 71 | |
| 72 | {{{ |
| 73 | #!python |
| 74 | class WeatherTemperatureBlock(FunctionalBlock): |
| 75 | |
| 76 | DP_01 = dict(name="temperature", access="output", dptId="9.001", default=19.) |
| 77 | DP_02 = dict(name="humidity", access="output", dptId="9.007", default=50.) |
| 78 | |
| 79 | GO_01 = dict(dp="temperature", flags="CRT", priority="low") |
| 80 | GO_02 = dict(dp="humidity", flags="CRT", priority="low") |
| 81 | }}} |
| 82 | |
| 83 | The {{{DP_xx}}} class attributes are the Datapoints of our Functional Block. The {{{GO_xx}}} class attributes are the Group Objects mapping the Datapoints to the bus. They are both defined as python dictionnary; they will be automatically instanciated for you by the framework. The named used here does not matter, as long as they start with {{{DP_}}} for Datapoints, and {{{GO_}}} for Group Objects. |
| 84 | |
| 85 | Then, we just instanciate our new Funtional Block: |
| 86 | |
| 87 | {{{ |
| 88 | #!python |
| 89 | weatherTempBlock = WeatherTemperatureBlock(name="weather_tempertature", desc="A simple weather block example") |
| 90 | }}} |
| 91 | |
| 92 | We also need to instanciante some global high level objects: |
| 93 | |
| 94 | {{{ |
| 95 | #!python |
| 96 | stack = Stack(individualAddress="1.2.3") |
| 97 | ets = ETS(stack) |
| 98 | }}} |
| 99 | |
| 100 | The {{{Stack}}} object is used to communicate over the bus (real bus, of course, but also on virtual bus). We can give it an Individual Address, to mimic real devices behaviour. |
| 101 | The {{{ETS}}} object is a tool which works like the reall ETS application. |
| 102 | |
| 103 | The {{{Stack}}} object is used to weave (bind, link...) our Group Objects to Group Addresses: |
| 104 | |
| 105 | {{{ |
| 106 | #!python |
| 107 | ets.weave(fb=weatherTempBlock, dp="temperature", gad="1/1/1") |
| 108 | ets.weave(fb=weatherTempBlock, dp="humidity", gad="1/1/2") |
| 109 | }}} |
| 110 | |
| 111 | And finally: |
| 112 | |
| 113 | {{{ |
| 114 | #!python |
| 115 | stack.serve() |
| 116 | }}} |
| 117 | |
| 118 | This call is blocking. We now have a process (Device) running, listening to the bus. The Datapoints, through their respective Group Objects, will react to requests on the Group Addresses their are weaved to ("1/1/1" and "1/1/2"). According to the flags, they will transmit their internal value on a Read request, or if this value changes. |
| 119 | |
| 120 | Ok, but our Functional Block is not really usefull yet, as it does not refresh its Datapoints! Let's see how it can be done: |
| 121 | |
| 122 | {{{ |
| 123 | #!python |
| 124 | from pknyx.api import FunctionalBlock, Stack, ETS |
| 125 | from pknyx.api import Scheduler |
| 126 | |
| 127 | |
| 128 | schedule = Scheduler() |
| 129 | |
| 130 | |
| 131 | class WeatherTemperatureBlock(FunctionalBlock): |
| 132 | |
| 133 | DP_01 = dict(name="temperature", access="output", dptId="9.001", default=19.) |
| 134 | DP_02 = dict(name="humidity", access="output", dptId="9.007", default=50.) |
| 135 | |
| 136 | GO_01 = dict(dp="temperature", flags="CRT", priority="low") |
| 137 | GO_02 = dict(dp="humidity", flags="CRT", priority="low") |
| 138 | |
| 139 | schedule.every(minute=5) |
| 140 | def updateTemperaturHumidity(self, event): |
| 141 | |
| 142 | # How we retreive the temperature/humidity is out of the scope of this proposal |
| 143 | # temperature = xxx |
| 144 | # humidity = xxx |
| 145 | self.dp["temperature"] = temperature |
| 146 | self.dp["humidity"] = humidity |
| 147 | |
| 148 | weatherTempBlock = WeatherTemperatureBlock(name="weather_tempertature", desc="A simple weather block example") |
| 149 | |
| 150 | stack = Stack(individualAddress="1.2.3") |
| 151 | ets = ETS(stack) |
| 152 | |
| 153 | ets.weave(fb=weatherTempBlock, dp="temperature", gad="1/1/1") |
| 154 | ets.weave(fb=weatherTempBlock, dp="humidity", gad="1/1/2") |
| 155 | |
| 156 | stack.run() |
| 157 | }}} |
| 158 | |
| 159 | All we do, here, is adding a method periodically called by the framework (every 5 minutes in the above example). In this method, we retreive the temperature and humidity values (not explained here), and give the value to the respective Datapoints. If the value has changed from the previous call, the Datapoint will automagically transmit it to the bus (according to the flags, of course). |