|
@@ -0,0 +1,250 @@
|
|
1
|
+from module import mqtt_test_client, init_state, state_change_by_mqtt
|
|
2
|
+from devices import shelly as test_device
|
|
3
|
+from devices import warning
|
|
4
|
+from mqtt import mqtt_client
|
|
5
|
+import pytest
|
|
6
|
+import time
|
|
7
|
+
|
|
8
|
+DUT_CLIENT_ID = "__%s__" % __name__
|
|
9
|
+TOPIC = "__test__/%s" % __name__
|
|
10
|
+#
|
|
11
|
+MQTT_SIGNAL_TIME = 0.2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+ALL_STATE_KEYS = ["relay/0", "relay/1", "input/0", "input/1", "longpush/0", "longpush/1", "temperature", "overtemperature"]
|
|
15
|
+BOOL_KEYS = ["relay/0", "relay/1", "input/0", "input/1", "longpush/0", "longpush/1", "overtemperature"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+@pytest.fixture
|
|
19
|
+def this_device():
|
|
20
|
+ mc = mqtt_client(DUT_CLIENT_ID, 'localhost')
|
|
21
|
+ return test_device(mc, TOPIC)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+def test_initial_states(this_device: test_device):
|
|
25
|
+ # test all initial values
|
|
26
|
+ init_state(ALL_STATE_KEYS, this_device)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+def test_state_change_by_mqtt(this_device: test_device):
|
|
30
|
+ def state_data(key):
|
|
31
|
+ if key in BOOL_KEYS:
|
|
32
|
+ return (True, False)
|
|
33
|
+ elif key == "temperature":
|
|
34
|
+ return (85.3, 20.1)
|
|
35
|
+ else:
|
|
36
|
+ raise IndexError("No return value defined for key %s" % key)
|
|
37
|
+
|
|
38
|
+ def mqtt_data(key):
|
|
39
|
+ if key in ["relay/0", "relay/1"]:
|
|
40
|
+ return ('on', 'off')
|
|
41
|
+ elif key in ["input/0", "input/1", "longpush/0", "longpush/1", "overtemperature"]:
|
|
42
|
+ return (1, 0)
|
|
43
|
+ else:
|
|
44
|
+ return state_data(key)
|
|
45
|
+
|
|
46
|
+ def warning_condition(state_topic, value):
|
|
47
|
+ return state_topic == "overtemperature" and value == 1
|
|
48
|
+
|
|
49
|
+ # test state changes
|
|
50
|
+ tm_warning = state_change_by_mqtt(ALL_STATE_KEYS, 2, mqtt_test_client, TOPIC, this_device,
|
|
51
|
+ mqtt_data, state_data, warning_condition, MQTT_SIGNAL_TIME)
|
|
52
|
+
|
|
53
|
+ # test warning
|
|
54
|
+ w: warning = this_device.get(this_device.KEY_WARNING)
|
|
55
|
+ assert w.get(w.KEY_ID) == TOPIC
|
|
56
|
+ assert w.get(w.KEY_TYPE) == w.TYPE_OVERTEMPERATURE
|
|
57
|
+ wt = time.mktime(w.get(w.KEY_TM))
|
|
58
|
+ wt_min = tm_warning
|
|
59
|
+ wt_max = tm_warning + 2
|
|
60
|
+ assert wt >= wt_min and wt <= wt_max
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+def test_specific_get_functions(this_device: test_device):
|
|
64
|
+ assert this_device.output_0 == this_device.get(this_device.KEY_OUTPUT_0)
|
|
65
|
+ assert this_device.output_1 == this_device.get(this_device.KEY_OUTPUT_1)
|
|
66
|
+ assert this_device.input_0 == this_device.get(this_device.KEY_INPUT_0)
|
|
67
|
+ assert this_device.input_1 == this_device.get(this_device.KEY_INPUT_1)
|
|
68
|
+ assert this_device.longpush_0 == this_device.get(this_device.KEY_LONGPUSH_0)
|
|
69
|
+ assert this_device.longpush_1 == this_device.get(this_device.KEY_LONGPUSH_1)
|
|
70
|
+ assert this_device.temperature == this_device.get(this_device.KEY_TEMPERATURE)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+def test_send_command(this_device: test_device):
|
|
74
|
+ this_device.set_output_0(True)
|
|
75
|
+ this_device.set_output_0(False)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+'''
|
|
79
|
+class shelly(base):
|
|
80
|
+ """ Communication (MQTT)
|
|
81
|
+
|
|
82
|
+ shelly
|
|
83
|
+ +- relay
|
|
84
|
+ | +- 0 ["on" / "off"] <- status
|
|
85
|
+ | | +- command ["on"/ "off"] <- command
|
|
86
|
+ | | +- energy [numeric] <- status
|
|
87
|
+ | +- 1 ["on" / "off"] <- status
|
|
88
|
+ | +- command ["on"/ "off"] <- command
|
|
89
|
+ | +- energy [numeric] <- status
|
|
90
|
+ +- input
|
|
91
|
+ | +- 0 [0 / 1] <- status
|
|
92
|
+ | +- 1 [0 / 1] <- status
|
|
93
|
+ +- input_event
|
|
94
|
+ | +- 0 <- status
|
|
95
|
+ | +- 1 <- status
|
|
96
|
+ +- logpush
|
|
97
|
+ | +- 0 [0 / 1] <- status
|
|
98
|
+ | +- 1 [0 / 1] <- status
|
|
99
|
+ +- temperature [numeric] °C <- status
|
|
100
|
+ +- temperature_f [numeric] F <- status
|
|
101
|
+ +- overtemperature [0 / 1] <- status
|
|
102
|
+ +- id <- status
|
|
103
|
+ +- model <- status
|
|
104
|
+ +- mac <- status
|
|
105
|
+ +- ip <- status
|
|
106
|
+ +- new_fw <- status
|
|
107
|
+ +- fw_ver <- status
|
|
108
|
+ """
|
|
109
|
+ KEY_OUTPUT_0 = "relay/0"
|
|
110
|
+ KEY_OUTPUT_1 = "relay/1"
|
|
111
|
+ KEY_INPUT_0 = "input/0"
|
|
112
|
+ KEY_INPUT_1 = "input/1"
|
|
113
|
+ KEY_LONGPUSH_0 = "longpush/0"
|
|
114
|
+ KEY_LONGPUSH_1 = "longpush/1"
|
|
115
|
+ KEY_TEMPERATURE = "temperature"
|
|
116
|
+ KEY_OVERTEMPERATURE = "overtemperature"
|
|
117
|
+ KEY_ID = "id"
|
|
118
|
+ KEY_MODEL = "model"
|
|
119
|
+ KEY_MAC = "mac"
|
|
120
|
+ KEY_IP = "ip"
|
|
121
|
+ KEY_NEW_FIRMWARE = "new_fw"
|
|
122
|
+ KEY_FIRMWARE_VERSION = "fw_ver"
|
|
123
|
+ #
|
|
124
|
+ TX_TOPIC = "command"
|
|
125
|
+ TX_TYPE = base.TX_VALUE
|
|
126
|
+ TX_FILTER_DATA_KEYS = [KEY_OUTPUT_0, KEY_OUTPUT_1]
|
|
127
|
+ #
|
|
128
|
+ RX_KEYS = [KEY_OUTPUT_0, KEY_OUTPUT_1, KEY_INPUT_0, KEY_INPUT_1, KEY_LONGPUSH_0, KEY_LONGPUSH_1, KEY_OVERTEMPERATURE, KEY_TEMPERATURE,
|
|
129
|
+ KEY_ID, KEY_MODEL, KEY_MAC, KEY_IP, KEY_NEW_FIRMWARE, KEY_FIRMWARE_VERSION]
|
|
130
|
+ RX_IGNORE_TOPICS = [KEY_OUTPUT_0 + '/' + "energy", KEY_OUTPUT_1 + '/' + "energy", 'input_event/0', 'input_event/1']
|
|
131
|
+ RX_IGNORE_KEYS = ['temperature_f']
|
|
132
|
+ RX_FILTER_DATA_KEYS = [KEY_INPUT_0, KEY_INPUT_1, KEY_LONGPUSH_0, KEY_LONGPUSH_1, KEY_OUTPUT_0, KEY_OUTPUT_1, KEY_OVERTEMPERATURE]
|
|
133
|
+
|
|
134
|
+ def __init__(self, mqtt_client, topic):
|
|
135
|
+ super().__init__(mqtt_client, topic)
|
|
136
|
+ #
|
|
137
|
+ self.output_key_delayed = None
|
|
138
|
+ self.delayed_flash_task = task.delayed(0.3, self.flash_task)
|
|
139
|
+ self.delayed_off_task = task.delayed(0.3, self.off_task)
|
|
140
|
+ #
|
|
141
|
+ self.add_callback(self.KEY_OVERTEMPERATURE, True, self.__warning__, True)
|
|
142
|
+ #
|
|
143
|
+ self.all_off_requested = False
|
|
144
|
+
|
|
145
|
+ def flash_task(self, *args):
|
|
146
|
+ if self.flash_active:
|
|
147
|
+ self.send_command(self.output_key_delayed, not self.get(self.output_key_delayed))
|
|
148
|
+ self.output_key_delayed = None
|
|
149
|
+ if self.all_off_requested:
|
|
150
|
+ self.delayed_off_task.run()
|
|
151
|
+
|
|
152
|
+ def off_task(self, *args):
|
|
153
|
+ self.all_off()
|
|
154
|
+
|
|
155
|
+ @property
|
|
156
|
+ def flash_active(self):
|
|
157
|
+ return self.output_key_delayed is not None
|
|
158
|
+
|
|
159
|
+ #
|
|
160
|
+ # WARNING CALL
|
|
161
|
+ #
|
|
162
|
+ def __warning__(self, client, key, data):
|
|
163
|
+ w = warning(self.topic, warning.TYPE_OVERTEMPERATURE, "Temperature to high (%.1f°C)", self.get(self.KEY_TEMPERATURE) or math.nan)
|
|
164
|
+ self.logger.warning(w)
|
|
165
|
+ self.set(self.KEY_WARNING, w)
|
|
166
|
+
|
|
167
|
+ #
|
|
168
|
+ # RX
|
|
169
|
+ #
|
|
170
|
+ @property
|
|
171
|
+ def output_0(self):
|
|
172
|
+ """rv: [True, False]"""
|
|
173
|
+ return self.get(self.KEY_OUTPUT_0)
|
|
174
|
+
|
|
175
|
+ @property
|
|
176
|
+ def output_1(self):
|
|
177
|
+ """rv: [True, False]"""
|
|
178
|
+ return self.get(self.KEY_OUTPUT_1)
|
|
179
|
+
|
|
180
|
+ @property
|
|
181
|
+ def input_0(self):
|
|
182
|
+ """rv: [True, False]"""
|
|
183
|
+ return self.get(self.KEY_INPUT_0)
|
|
184
|
+
|
|
185
|
+ @property
|
|
186
|
+ def input_1(self):
|
|
187
|
+ """rv: [True, False]"""
|
|
188
|
+ return self.get(self.KEY_INPUT_1)
|
|
189
|
+
|
|
190
|
+ @property
|
|
191
|
+ def longpush_0(self):
|
|
192
|
+ """rv: [True, False]"""
|
|
193
|
+ return self.get(self.KEY_LONGPUSH_0)
|
|
194
|
+
|
|
195
|
+ @property
|
|
196
|
+ def longpush_1(self):
|
|
197
|
+ """rv: [True, False]"""
|
|
198
|
+ return self.get(self.KEY_LONGPUSH_1)
|
|
199
|
+
|
|
200
|
+ @property
|
|
201
|
+ def temperature(self):
|
|
202
|
+ """rv: numeric value"""
|
|
203
|
+ return self.get(self.KEY_TEMPERATURE)
|
|
204
|
+
|
|
205
|
+ #
|
|
206
|
+ # TX
|
|
207
|
+ #
|
|
208
|
+ def set_output_0(self, state):
|
|
209
|
+ """state: [True, False]"""
|
|
210
|
+ self.send_command(self.KEY_OUTPUT_0, state)
|
|
211
|
+
|
|
212
|
+ def set_output_0_mcb(self, device, key, data):
|
|
213
|
+ self.logger.log(logging.INFO if data != self.output_0 else logging.DEBUG, "Changing output 0 to %s", str(data))
|
|
214
|
+ self.set_output_0(data)
|
|
215
|
+
|
|
216
|
+ def toggle_output_0_mcb(self, device, key, data):
|
|
217
|
+ self.logger.info("Toggeling output 0")
|
|
218
|
+ self.set_output_0(not self.output_0)
|
|
219
|
+
|
|
220
|
+ def set_output_1(self, state):
|
|
221
|
+ """state: [True, False]"""
|
|
222
|
+ self.send_command(self.KEY_OUTPUT_1, state)
|
|
223
|
+
|
|
224
|
+ def set_output_1_mcb(self, device, key, data):
|
|
225
|
+ self.logger.log(logging.INFO if data != self.output_1 else logging.DEBUG, "Changing output 1 to %s", str(data))
|
|
226
|
+ self.set_output_1(data)
|
|
227
|
+
|
|
228
|
+ def toggle_output_1_mcb(self, device, key, data):
|
|
229
|
+ self.logger.info("Toggeling output 1")
|
|
230
|
+ self.set_output_1(not self.output_1)
|
|
231
|
+
|
|
232
|
+ def flash_0_mcb(self, device, key, data):
|
|
233
|
+ self.output_key_delayed = self.KEY_OUTPUT_0
|
|
234
|
+ self.toggle_output_0_mcb(device, key, data)
|
|
235
|
+ self.delayed_flash_task.run()
|
|
236
|
+
|
|
237
|
+ def flash_1_mcb(self, device, key, data):
|
|
238
|
+ self.output_key_delayed = self.KEY_OUTPUT_1
|
|
239
|
+ self.toggle_output_1_mcb(device, key, data)
|
|
240
|
+ self.delayed_flash_task.run()
|
|
241
|
+
|
|
242
|
+ def all_off(self):
|
|
243
|
+ if self.flash_active:
|
|
244
|
+ self.all_off_requested = True
|
|
245
|
+ else:
|
|
246
|
+ if self.output_0:
|
|
247
|
+ self.set_output_0(False)
|
|
248
|
+ if self.output_1:
|
|
249
|
+ self.set_output_1(False)
|
|
250
|
+'''
|