dashio 3.6.2__tar.gz → 3.7.0__tar.gz
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.
- {dashio-3.6.2 → dashio-3.7.0}/PKG-INFO +1 -1
- {dashio-3.6.2 → dashio-3.7.0}/dashio/__init__.py +0 -4
- {dashio-3.6.2 → dashio-3.7.0}/dashio/dash_connection.py +53 -9
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/event.py +9 -3
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/event_log.py +3 -6
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/map.py +4 -4
- {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/PKG-INFO +1 -1
- {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/SOURCES.txt +1 -0
- {dashio-3.6.2 → dashio-3.7.0}/pyproject.toml +1 -1
- dashio-3.7.0/tests/test_dashconnection.py +63 -0
- dashio-3.7.0/tests/test_history_replies.py +87 -0
- dashio-3.6.2/tests/test_dashconnection.py +0 -31
- {dashio-3.6.2 → dashio-3.7.0}/Examples/RaspberryPi/TestBLE.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/RaspberryPi/ble_set_wifi.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/RaspberryPi/pi_sense_hat.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/RaspberryPi/pi_sense_hat_tsgraph.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/alarms.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/all_controls.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/as_test.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/bbb_temperature.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/dashio_coms_module.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/dashio_test.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/dual_connection_TCP_DASH.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/event_log.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/ex00.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/ex01.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/ex02.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/ex03.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/ex04.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/ex05_SIM767X.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/greenhouse/greenhouse.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/greenhouse/simulator.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/map.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/server_monitor/server_monitor.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/system_monitor.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/tcp_example.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/Examples/time_graphs.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/LICENSE +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/README.md +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/ble_connection.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/comms_module_connection.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/constants.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/device.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/__init__.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/alarm.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/audio_visual_display.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/button.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/button_group.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/chart.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/color_picker.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/control.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/device_view.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/dial.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/direction.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/enums.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/knob.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/label.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/menu.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/py.typed +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/ring_buffer.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/selector.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/slider.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/table.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/textbox.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/time_graph.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/ip.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/load_config.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/lte_767x_connection.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/py.typed +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/quectel_EG800Q.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/schedular.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/sim767x.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/tcp_connection.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/telit_LE910Q1.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/utilities/__init__.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/utilities/c64_decode.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/utilities/c64_encode.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/utilities/dashio_data_exporter.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/zeroconf_service.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio/zmq_connection.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/dependency_links.txt +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/entry_points.txt +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/not-zip-safe +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/requires.txt +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/top_level.txt +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/setup.cfg +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_alarm.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_button.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_button_group.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_cfg_round_trip.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_chart.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_control.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_dashdevice.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_device_layouts.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_device_view.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_dial.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_direction.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_event_log.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_knob.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_label.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_map.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_menu.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_selector.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_slider.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_tcpconnection.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_textbox.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_time_graph.py +0 -0
- {dashio-3.6.2 → dashio-3.7.0}/tests/test_zqmconnection.py +0 -0
|
@@ -70,19 +70,15 @@ from .load_config import (decode_cfg64, encode_cfg64, generate_cfg64,
|
|
|
70
70
|
get_control_dict_from_config,
|
|
71
71
|
get_control_from_config,
|
|
72
72
|
load_all_controls_from_config)
|
|
73
|
-
from .lte_767x_connection import Lte767xConnection
|
|
74
73
|
from .schedular import Schedular
|
|
75
74
|
from .tcp_connection import TCPConnection
|
|
76
75
|
from .zmq_connection import ZMQConnection
|
|
77
|
-
from .quectel_EG800Q import EG800Q
|
|
78
76
|
|
|
79
77
|
__all__ = [
|
|
80
78
|
'Device',
|
|
81
79
|
'TCPConnection',
|
|
82
80
|
'ZMQConnection',
|
|
83
81
|
'DashConnection',
|
|
84
|
-
'Lte767xConnection',
|
|
85
|
-
'EG800Q',
|
|
86
82
|
'ConnectionState',
|
|
87
83
|
'DashIOCommsModuleConnection',
|
|
88
84
|
'Schedular',
|
|
@@ -31,6 +31,8 @@ import time
|
|
|
31
31
|
from socket import gaierror
|
|
32
32
|
|
|
33
33
|
import paho.mqtt.client as mqtt
|
|
34
|
+
from paho.mqtt.packettypes import PacketTypes
|
|
35
|
+
from paho.mqtt.properties import Properties
|
|
34
36
|
import shortuuid
|
|
35
37
|
import zmq
|
|
36
38
|
|
|
@@ -148,6 +150,8 @@ class DashConnection(threading.Thread):
|
|
|
148
150
|
for device_id in self._device_id_rx_list:
|
|
149
151
|
data_topic = f"{self.username}/{device_id}/data"
|
|
150
152
|
result, mid = self._dash_c.subscribe(data_topic, 0)
|
|
153
|
+
for device_id in self._device_id_list:
|
|
154
|
+
self._send_online_state(device_id, "ONLINE")
|
|
151
155
|
self._disconnect_timeout = 1.0
|
|
152
156
|
logger.debug("connected OK")
|
|
153
157
|
else:
|
|
@@ -192,6 +196,7 @@ class DashConnection(threading.Thread):
|
|
|
192
196
|
result, mid = self._dash_c.subscribe(control_topic, 0)
|
|
193
197
|
if result == mqtt.MQTT_ERR_SUCCESS:
|
|
194
198
|
self._subscribing_topics[mid] = control_topic
|
|
199
|
+
self._send_online_state(device.device_id, "ONLINE")
|
|
195
200
|
|
|
196
201
|
def _add_device_rx(self, msg_dict):
|
|
197
202
|
"""Connect to another device"""
|
|
@@ -219,11 +224,38 @@ class DashConnection(threading.Thread):
|
|
|
219
224
|
logger.debug("DASH SEND ANNOUNCE: %s", msg)
|
|
220
225
|
self.tx_zmq_pub.send_multipart([b"COMMAND", json.dumps(msg).encode()])
|
|
221
226
|
|
|
227
|
+
def _send_online_state(self, device_id: str, state: str):
|
|
228
|
+
"""Publish ONLINE or OFFLINE for device_id to its data topic."""
|
|
229
|
+
return self._dash_c.publish(f"{self.username}/{device_id}/data", f"\t{device_id}\t{state}\n", qos=1)
|
|
230
|
+
|
|
231
|
+
def _set_will(self):
|
|
232
|
+
"""Set the MQTT Last Will and Testament: the Dash server publishes OFFLINE if the connection is lost.
|
|
233
|
+
|
|
234
|
+
An MQTT connection has a single will on a single topic, so it is for the first device on the connection.
|
|
235
|
+
The will is fixed when connecting, so devices added later aren't included. close() sends OFFLINE for
|
|
236
|
+
every device.
|
|
237
|
+
"""
|
|
238
|
+
if not self._device_id_list:
|
|
239
|
+
self._dash_c.will_clear()
|
|
240
|
+
return
|
|
241
|
+
device_id = self._device_id_list[0]
|
|
242
|
+
properties = None
|
|
243
|
+
if self.offline_delay > 0:
|
|
244
|
+
properties = Properties(PacketTypes.WILLMESSAGE)
|
|
245
|
+
properties.WillDelayInterval = self.offline_delay
|
|
246
|
+
self._dash_c.will_set(f"{self.username}/{device_id}/data", f"\t{device_id}\tOFFLINE\n", qos=1, retain=False, properties=properties)
|
|
247
|
+
|
|
222
248
|
def connect(self):
|
|
223
249
|
"""Connect to the server."""
|
|
224
250
|
logger.debug("Connecting..")
|
|
251
|
+
self._set_will()
|
|
252
|
+
properties = None
|
|
253
|
+
if self.offline_delay > 0:
|
|
254
|
+
# The server only waits the will delay if it keeps the session that long.
|
|
255
|
+
properties = Properties(PacketTypes.CONNECT)
|
|
256
|
+
properties.SessionExpiryInterval = self.offline_delay
|
|
225
257
|
try:
|
|
226
|
-
self._dash_c.connect(self.host, self.port)
|
|
258
|
+
self._dash_c.connect(self.host, self.port, properties=properties)
|
|
227
259
|
self.connection_state = ConnectionState.CONNECTING
|
|
228
260
|
# Start the network loop only once a host is set, otherwise paho's thread dies with "Invalid host".
|
|
229
261
|
self._dash_c.loop_start()
|
|
@@ -251,7 +283,8 @@ class DashConnection(threading.Thread):
|
|
|
251
283
|
host='dash.dashio.io',
|
|
252
284
|
port=8883,
|
|
253
285
|
use_ssl=True,
|
|
254
|
-
context: zmq.Context | None = None
|
|
286
|
+
context: zmq.Context | None = None,
|
|
287
|
+
offline_delay: int = 10
|
|
255
288
|
):
|
|
256
289
|
"""
|
|
257
290
|
Setups and manages a connection thread to the Dash Server.
|
|
@@ -268,6 +301,11 @@ class DashConnection(threading.Thread):
|
|
|
268
301
|
password for the dash connection.
|
|
269
302
|
use_ssl : Boolean
|
|
270
303
|
Defaults to True.
|
|
304
|
+
context : zmq.Context, optional
|
|
305
|
+
ZMQ context. Defaults to the shared zmq.Context.instance().
|
|
306
|
+
offline_delay : int, optional
|
|
307
|
+
If the connection is lost, the Dash server publishes OFFLINE for the first device after this many
|
|
308
|
+
seconds, unless the connection is restored first. 0 publishes it straight away. Defaults to 10.
|
|
271
309
|
"""
|
|
272
310
|
|
|
273
311
|
threading.Thread.__init__(self, daemon=True)
|
|
@@ -279,7 +317,7 @@ class DashConnection(threading.Thread):
|
|
|
279
317
|
self._device_id_list = []
|
|
280
318
|
self._device_id_rx_list = []
|
|
281
319
|
self._subscribing_topics = {}
|
|
282
|
-
|
|
320
|
+
self.offline_delay = offline_delay
|
|
283
321
|
self.running = True
|
|
284
322
|
self.username = username
|
|
285
323
|
self.password = password
|
|
@@ -305,18 +343,24 @@ class DashConnection(threading.Thread):
|
|
|
305
343
|
self._dash_c.tls_insecure_set(False)
|
|
306
344
|
|
|
307
345
|
self._dash_c.username_pw_set(self.username, self.password)
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
# Connect
|
|
311
|
-
if username and password:
|
|
312
|
-
self.connect()
|
|
346
|
+
# The connection thread connects, a moment after it starts, so the devices added straight after creating
|
|
347
|
+
# the connection are included in the will.
|
|
313
348
|
# Start subscribe, with QoS level 0
|
|
314
349
|
self.rx_zmq_sub = self.context.socket(zmq.SUB)
|
|
315
350
|
self._disconnect_timeout = 1.0
|
|
316
351
|
self.start()
|
|
317
352
|
|
|
318
353
|
def close(self):
|
|
319
|
-
"""
|
|
354
|
+
"""Send OFFLINE for every device, then close the connection."""
|
|
355
|
+
if self.connection_state == ConnectionState.CONNECTED:
|
|
356
|
+
messages = [self._send_online_state(device_id, "OFFLINE") for device_id in self._device_id_list]
|
|
357
|
+
for message in messages:
|
|
358
|
+
try:
|
|
359
|
+
message.wait_for_publish(timeout=2.0)
|
|
360
|
+
except (RuntimeError, ValueError) as error:
|
|
361
|
+
logger.debug("OFFLINE not sent: %s", error)
|
|
362
|
+
# A normal disconnect, so the server doesn't also publish the will.
|
|
363
|
+
self._dash_c.disconnect()
|
|
320
364
|
self.running = False
|
|
321
365
|
|
|
322
366
|
def _dash_command(self, msg_dict: dict):
|
|
@@ -70,11 +70,17 @@ class Event:
|
|
|
70
70
|
raise ValueError("Handler is not handling this event, so cannot unhandle it.") from not_in_set
|
|
71
71
|
return self
|
|
72
72
|
|
|
73
|
-
def fire(self, *args, **kargs):
|
|
73
|
+
def fire(self, *args, **kargs) -> str:
|
|
74
74
|
"""Runs all the handlers
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
str
|
|
79
|
+
The strings returned by the handlers joined together, e.g. replies to send to the **Dash** app.
|
|
80
|
+
Handlers that return anything else are ignored.
|
|
75
81
|
"""
|
|
76
|
-
for handler in self.handlers
|
|
77
|
-
|
|
82
|
+
replies = [handler(*args, **kargs) for handler in self.handlers]
|
|
83
|
+
return "".join(reply for reply in replies if isinstance(reply, str))
|
|
78
84
|
|
|
79
85
|
def get_handler_count(self):
|
|
80
86
|
"""Returns the number of handlers
|
|
@@ -143,15 +143,12 @@ class EventLog(Control):
|
|
|
143
143
|
from_date = dateutil.parser.isoparse(msg[4])
|
|
144
144
|
except (IndexError, ValueError):
|
|
145
145
|
return ""
|
|
146
|
-
data_str += self._control_hdr_str + dashboard_id
|
|
147
|
-
has_data = False
|
|
148
146
|
for log in self.log.get():
|
|
149
147
|
if log.timestamp > from_date:
|
|
150
148
|
data_str += "\t" + log.to_json()
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return data_str
|
|
149
|
+
if not data_str:
|
|
150
|
+
return ""
|
|
151
|
+
return self._control_hdr_str + dashboard_id + data_str + "\n"
|
|
155
152
|
|
|
156
153
|
def add_event_data(self, data: EventData):
|
|
157
154
|
"""Add a data point to the log and send it to any connected **Dash** app
|
|
@@ -126,7 +126,7 @@ class MapTrack:
|
|
|
126
126
|
"""
|
|
127
127
|
reply = f"\t{self.track_id}\t{self.text}\t{self.color.value}"
|
|
128
128
|
for loc in self.locations:
|
|
129
|
-
reply += f"\t{loc.
|
|
129
|
+
reply += f"\t{loc.to_json()}"
|
|
130
130
|
return reply
|
|
131
131
|
|
|
132
132
|
def get_last_location(self) -> str:
|
|
@@ -199,10 +199,10 @@ class Map(Control):
|
|
|
199
199
|
try:
|
|
200
200
|
dashboard_id = msg[3]
|
|
201
201
|
from_timestamp = parser.isoparse(msg[4])
|
|
202
|
-
except IndexError:
|
|
202
|
+
except (IndexError, ValueError):
|
|
203
203
|
return ""
|
|
204
204
|
if self.tracks:
|
|
205
|
-
for
|
|
205
|
+
for value in self.tracks.values():
|
|
206
206
|
if value.track_start_time > from_timestamp:
|
|
207
207
|
reply += self._control_hdr_str + dashboard_id + value.get_track() + "\n"
|
|
208
208
|
return reply
|
|
@@ -219,7 +219,7 @@ class Map(Control):
|
|
|
219
219
|
if track_id in self.tracks:
|
|
220
220
|
self.tracks[track_id].add_location(location)
|
|
221
221
|
else:
|
|
222
|
-
self.tracks[track_id] = MapTrack(
|
|
222
|
+
self.tracks[track_id] = MapTrack(track_id)
|
|
223
223
|
self.tracks[track_id].add_location(location)
|
|
224
224
|
self.send_location(location, track_id)
|
|
225
225
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
import zmq
|
|
5
|
+
|
|
6
|
+
from dashio import DashConnection, Device, decode_cfg64
|
|
7
|
+
from dashio.dash_connection import DashControl
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestDashConnection(unittest.TestCase):
|
|
11
|
+
def _get_cfg_dict(self, cfg_str):
|
|
12
|
+
json_str = cfg_str.rpartition('\t')[2]
|
|
13
|
+
return json.loads(json_str)
|
|
14
|
+
|
|
15
|
+
def test_dash_connection_username(self):
|
|
16
|
+
# Connect to localhost so the test doesn't contact the Dash server.
|
|
17
|
+
test_connection = DashConnection("USERNAME", "PASSWORD", host="localhost")
|
|
18
|
+
self.assertEqual(test_connection.username, 'USERNAME', "username type should be USERNAME")
|
|
19
|
+
test_connection.close()
|
|
20
|
+
|
|
21
|
+
def test_dash_control_cfg_in_device(self):
|
|
22
|
+
device = Device("DEVICETYPE", "DEVICEID", "DEVICENAME", context=zmq.Context())
|
|
23
|
+
self.assertTrue(device.add_control(DashControl("DASH1", "USERNAME", "dash.dashio.io")))
|
|
24
|
+
reply = device._make_cfg64(["", "DEVICEID", "DASHID", "1"])
|
|
25
|
+
device.close()
|
|
26
|
+
cfg = decode_cfg64(reply.rstrip("\n").rpartition("\t")[2])
|
|
27
|
+
self.assertEqual(cfg["DASHIO"], {"controlID": "DASH1", "userName": "USERNAME", "hostURL": "dash.dashio.io"})
|
|
28
|
+
|
|
29
|
+
def test_will_is_offline_for_first_device(self):
|
|
30
|
+
# Connect to localhost so the test doesn't contact the Dash server.
|
|
31
|
+
test_connection = DashConnection("USERNAME", "PASSWORD", host="localhost", offline_delay=7)
|
|
32
|
+
first = Device("DEVICETYPE", "DEVICEID1", "DEVICENAME", context=zmq.Context())
|
|
33
|
+
second = Device("DEVICETYPE", "DEVICEID2", "DEVICENAME", context=zmq.Context())
|
|
34
|
+
test_connection.add_device(first)
|
|
35
|
+
test_connection.add_device(second)
|
|
36
|
+
test_connection._set_will()
|
|
37
|
+
client = test_connection._dash_c
|
|
38
|
+
self.assertEqual(client._will_topic, b"USERNAME/DEVICEID1/data", "The will should be on the first device's data topic")
|
|
39
|
+
self.assertEqual(client._will_payload, b"\tDEVICEID1\tOFFLINE\n")
|
|
40
|
+
self.assertEqual((client._will_qos, client._will_retain), (1, False))
|
|
41
|
+
self.assertEqual(client._will_properties.WillDelayInterval, 7)
|
|
42
|
+
test_connection.close()
|
|
43
|
+
first.close()
|
|
44
|
+
second.close()
|
|
45
|
+
|
|
46
|
+
def test_no_will_without_devices(self):
|
|
47
|
+
test_connection = DashConnection("USERNAME", "PASSWORD", host="localhost")
|
|
48
|
+
test_connection._set_will()
|
|
49
|
+
self.assertFalse(test_connection._dash_c._will, "There should be no will without a device")
|
|
50
|
+
test_connection.close()
|
|
51
|
+
|
|
52
|
+
def test_offline_delay_zero_has_no_will_delay(self):
|
|
53
|
+
test_connection = DashConnection("USERNAME", "PASSWORD", host="localhost", offline_delay=0)
|
|
54
|
+
device = Device("DEVICETYPE", "DEVICEID1", "DEVICENAME", context=zmq.Context())
|
|
55
|
+
test_connection.add_device(device)
|
|
56
|
+
test_connection._set_will()
|
|
57
|
+
self.assertIsNone(test_connection._dash_c._will_properties, "offline_delay=0 should publish the will straight away")
|
|
58
|
+
test_connection.close()
|
|
59
|
+
device.close()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == '__main__':
|
|
63
|
+
unittest.main()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
import dashio
|
|
6
|
+
from dashio.iotcontrol.event import Event
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def since(hours: float) -> str:
|
|
10
|
+
return (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(hours=hours)).isoformat()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestEvent(unittest.TestCase):
|
|
14
|
+
def test_fire_joins_string_replies(self):
|
|
15
|
+
event = Event()
|
|
16
|
+
event += lambda msg: "a\n"
|
|
17
|
+
event += lambda msg: None
|
|
18
|
+
event += lambda msg: True
|
|
19
|
+
self.assertEqual(event("msg"), "a\n", "String replies are returned and other return values ignored")
|
|
20
|
+
|
|
21
|
+
def test_fire_without_replies(self):
|
|
22
|
+
event = Event()
|
|
23
|
+
event += lambda msg: None
|
|
24
|
+
self.assertEqual(event("msg"), "")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestHistoryReplies(unittest.TestCase):
|
|
28
|
+
def setUp(self):
|
|
29
|
+
self.device = dashio.Device("DEVICETYPE", "DEVICEID", "DEVICENAME")
|
|
30
|
+
|
|
31
|
+
def tearDown(self):
|
|
32
|
+
self.device.close()
|
|
33
|
+
|
|
34
|
+
def request(self, ctrl_type: str, control_id: str, from_time: str) -> str:
|
|
35
|
+
return self.device._on_message(f"\tDEVICEID\t{ctrl_type}\t{control_id}\tDASHID\t{from_time}\n".encode())
|
|
36
|
+
|
|
37
|
+
def test_event_log_history(self):
|
|
38
|
+
log = dashio.EventLog("LOG1")
|
|
39
|
+
self.device.add_control(log)
|
|
40
|
+
log.log.append(dashio.EventData("Something happened"))
|
|
41
|
+
reply = self.request("LOG", "LOG1", since(1))
|
|
42
|
+
fields = reply.rstrip("\n").split("\t")
|
|
43
|
+
self.assertEqual(fields[1:5], ["DEVICEID", "LOG", "LOG1", "DASHID"])
|
|
44
|
+
self.assertEqual(json.loads(fields[5])["lines"], ["Something happened"])
|
|
45
|
+
self.assertTrue(reply.endswith("\n"))
|
|
46
|
+
|
|
47
|
+
def test_event_log_history_empty(self):
|
|
48
|
+
log = dashio.EventLog("LOG1")
|
|
49
|
+
self.device.add_control(log)
|
|
50
|
+
log.log.append(dashio.EventData("Something happened"))
|
|
51
|
+
self.assertEqual(self.request("LOG", "LOG1", since(-1)), "", "No reply when there are no events after the time")
|
|
52
|
+
|
|
53
|
+
def test_time_graph_history(self):
|
|
54
|
+
graph = dashio.TimeGraph("TG1")
|
|
55
|
+
graph.add_line("L1", dashio.TimeGraphLine("Line 1"))
|
|
56
|
+
self.device.add_control(graph)
|
|
57
|
+
graph.line_dict["L1"].add_data_point(1.5)
|
|
58
|
+
reply = self.request("TGRPH", "TG1", since(1))
|
|
59
|
+
fields = reply.rstrip("\n").split("\t")
|
|
60
|
+
self.assertEqual(fields[1:6], ["DEVICEID", "TGRPH", "TG1", "DASHID", "L1"])
|
|
61
|
+
self.assertTrue(fields[-1].endswith(",1.5"))
|
|
62
|
+
|
|
63
|
+
def test_map_history(self):
|
|
64
|
+
a_map = dashio.Map("MAP1")
|
|
65
|
+
self.device.add_control(a_map)
|
|
66
|
+
a_map.add_location_to_track(dashio.MapLocation("-45.0", "168.0"), "T1")
|
|
67
|
+
reply = self.request("MAP", "MAP1", since(1))
|
|
68
|
+
fields = reply.rstrip("\n").split("\t")
|
|
69
|
+
self.assertEqual(fields[1:6], ["DEVICEID", "MAP", "MAP1", "DASHID", "T1"], "The track should keep its track_id")
|
|
70
|
+
self.assertEqual(json.loads(fields[-1])["latitude"], "-45.0")
|
|
71
|
+
|
|
72
|
+
def test_map_history_bad_time(self):
|
|
73
|
+
a_map = dashio.Map("MAP1")
|
|
74
|
+
self.device.add_control(a_map)
|
|
75
|
+
a_map.add_location_to_track(dashio.MapLocation("-45.0", "168.0"), "T1")
|
|
76
|
+
self.assertEqual(self.request("MAP", "MAP1", "not a time"), "")
|
|
77
|
+
|
|
78
|
+
def test_receive_callback_reply(self):
|
|
79
|
+
log = dashio.EventLog("LOG1")
|
|
80
|
+
self.device.add_control(log)
|
|
81
|
+
log.add_receive_message_callback(lambda msg: f"\t{{device_id}}\tLOG\tLOG1\t{msg[3]}\t{{\"time\": \"x\"}}\n")
|
|
82
|
+
reply = self.request("LOG", "LOG1", since(1))
|
|
83
|
+
self.assertEqual(reply, "\tDEVICEID\tLOG\tLOG1\tDASHID\t{\"time\": \"x\"}\n", "A receive callback's reply should be sent")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == '__main__':
|
|
87
|
+
unittest.main()
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import unittest
|
|
3
|
-
|
|
4
|
-
import zmq
|
|
5
|
-
|
|
6
|
-
from dashio import DashConnection, Device, decode_cfg64
|
|
7
|
-
from dashio.dash_connection import DashControl
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class TestDashConnection(unittest.TestCase):
|
|
11
|
-
def _get_cfg_dict(self, cfg_str):
|
|
12
|
-
json_str = cfg_str.rpartition('\t')[2]
|
|
13
|
-
return json.loads(json_str)
|
|
14
|
-
|
|
15
|
-
def test_dash_connection_username(self):
|
|
16
|
-
# Connect to localhost so the test doesn't contact the Dash server.
|
|
17
|
-
test_connection = DashConnection("USERNAME", "PASSWORD", host="localhost")
|
|
18
|
-
self.assertEqual(test_connection.username, 'USERNAME', "username type should be USERNAME")
|
|
19
|
-
test_connection.close()
|
|
20
|
-
|
|
21
|
-
def test_dash_control_cfg_in_device(self):
|
|
22
|
-
device = Device("DEVICETYPE", "DEVICEID", "DEVICENAME", context=zmq.Context())
|
|
23
|
-
self.assertTrue(device.add_control(DashControl("DASH1", "USERNAME", "dash.dashio.io")))
|
|
24
|
-
reply = device._make_cfg64(["", "DEVICEID", "DASHID", "1"])
|
|
25
|
-
device.close()
|
|
26
|
-
cfg = decode_cfg64(reply.rstrip("\n").rpartition("\t")[2])
|
|
27
|
-
self.assertEqual(cfg["DASHIO"], {"controlID": "DASH1", "userName": "USERNAME", "hostURL": "dash.dashio.io"})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if __name__ == '__main__':
|
|
31
|
-
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|