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.
Files changed (108) hide show
  1. {dashio-3.6.2 → dashio-3.7.0}/PKG-INFO +1 -1
  2. {dashio-3.6.2 → dashio-3.7.0}/dashio/__init__.py +0 -4
  3. {dashio-3.6.2 → dashio-3.7.0}/dashio/dash_connection.py +53 -9
  4. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/event.py +9 -3
  5. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/event_log.py +3 -6
  6. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/map.py +4 -4
  7. {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/PKG-INFO +1 -1
  8. {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/SOURCES.txt +1 -0
  9. {dashio-3.6.2 → dashio-3.7.0}/pyproject.toml +1 -1
  10. dashio-3.7.0/tests/test_dashconnection.py +63 -0
  11. dashio-3.7.0/tests/test_history_replies.py +87 -0
  12. dashio-3.6.2/tests/test_dashconnection.py +0 -31
  13. {dashio-3.6.2 → dashio-3.7.0}/Examples/RaspberryPi/TestBLE.py +0 -0
  14. {dashio-3.6.2 → dashio-3.7.0}/Examples/RaspberryPi/ble_set_wifi.py +0 -0
  15. {dashio-3.6.2 → dashio-3.7.0}/Examples/RaspberryPi/pi_sense_hat.py +0 -0
  16. {dashio-3.6.2 → dashio-3.7.0}/Examples/RaspberryPi/pi_sense_hat_tsgraph.py +0 -0
  17. {dashio-3.6.2 → dashio-3.7.0}/Examples/alarms.py +0 -0
  18. {dashio-3.6.2 → dashio-3.7.0}/Examples/all_controls.py +0 -0
  19. {dashio-3.6.2 → dashio-3.7.0}/Examples/as_test.py +0 -0
  20. {dashio-3.6.2 → dashio-3.7.0}/Examples/bbb_temperature.py +0 -0
  21. {dashio-3.6.2 → dashio-3.7.0}/Examples/dashio_coms_module.py +0 -0
  22. {dashio-3.6.2 → dashio-3.7.0}/Examples/dashio_test.py +0 -0
  23. {dashio-3.6.2 → dashio-3.7.0}/Examples/dual_connection_TCP_DASH.py +0 -0
  24. {dashio-3.6.2 → dashio-3.7.0}/Examples/event_log.py +0 -0
  25. {dashio-3.6.2 → dashio-3.7.0}/Examples/ex00.py +0 -0
  26. {dashio-3.6.2 → dashio-3.7.0}/Examples/ex01.py +0 -0
  27. {dashio-3.6.2 → dashio-3.7.0}/Examples/ex02.py +0 -0
  28. {dashio-3.6.2 → dashio-3.7.0}/Examples/ex03.py +0 -0
  29. {dashio-3.6.2 → dashio-3.7.0}/Examples/ex04.py +0 -0
  30. {dashio-3.6.2 → dashio-3.7.0}/Examples/ex05_SIM767X.py +0 -0
  31. {dashio-3.6.2 → dashio-3.7.0}/Examples/greenhouse/greenhouse.py +0 -0
  32. {dashio-3.6.2 → dashio-3.7.0}/Examples/greenhouse/simulator.py +0 -0
  33. {dashio-3.6.2 → dashio-3.7.0}/Examples/map.py +0 -0
  34. {dashio-3.6.2 → dashio-3.7.0}/Examples/server_monitor/server_monitor.py +0 -0
  35. {dashio-3.6.2 → dashio-3.7.0}/Examples/system_monitor.py +0 -0
  36. {dashio-3.6.2 → dashio-3.7.0}/Examples/tcp_example.py +0 -0
  37. {dashio-3.6.2 → dashio-3.7.0}/Examples/time_graphs.py +0 -0
  38. {dashio-3.6.2 → dashio-3.7.0}/LICENSE +0 -0
  39. {dashio-3.6.2 → dashio-3.7.0}/README.md +0 -0
  40. {dashio-3.6.2 → dashio-3.7.0}/dashio/ble_connection.py +0 -0
  41. {dashio-3.6.2 → dashio-3.7.0}/dashio/comms_module_connection.py +0 -0
  42. {dashio-3.6.2 → dashio-3.7.0}/dashio/constants.py +0 -0
  43. {dashio-3.6.2 → dashio-3.7.0}/dashio/device.py +0 -0
  44. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/__init__.py +0 -0
  45. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/alarm.py +0 -0
  46. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/audio_visual_display.py +0 -0
  47. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/button.py +0 -0
  48. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/button_group.py +0 -0
  49. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/chart.py +0 -0
  50. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/color_picker.py +0 -0
  51. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/control.py +0 -0
  52. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/device_view.py +0 -0
  53. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/dial.py +0 -0
  54. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/direction.py +0 -0
  55. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/enums.py +0 -0
  56. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/knob.py +0 -0
  57. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/label.py +0 -0
  58. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/menu.py +0 -0
  59. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/py.typed +0 -0
  60. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/ring_buffer.py +0 -0
  61. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/selector.py +0 -0
  62. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/slider.py +0 -0
  63. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/table.py +0 -0
  64. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/textbox.py +0 -0
  65. {dashio-3.6.2 → dashio-3.7.0}/dashio/iotcontrol/time_graph.py +0 -0
  66. {dashio-3.6.2 → dashio-3.7.0}/dashio/ip.py +0 -0
  67. {dashio-3.6.2 → dashio-3.7.0}/dashio/load_config.py +0 -0
  68. {dashio-3.6.2 → dashio-3.7.0}/dashio/lte_767x_connection.py +0 -0
  69. {dashio-3.6.2 → dashio-3.7.0}/dashio/py.typed +0 -0
  70. {dashio-3.6.2 → dashio-3.7.0}/dashio/quectel_EG800Q.py +0 -0
  71. {dashio-3.6.2 → dashio-3.7.0}/dashio/schedular.py +0 -0
  72. {dashio-3.6.2 → dashio-3.7.0}/dashio/sim767x.py +0 -0
  73. {dashio-3.6.2 → dashio-3.7.0}/dashio/tcp_connection.py +0 -0
  74. {dashio-3.6.2 → dashio-3.7.0}/dashio/telit_LE910Q1.py +0 -0
  75. {dashio-3.6.2 → dashio-3.7.0}/dashio/utilities/__init__.py +0 -0
  76. {dashio-3.6.2 → dashio-3.7.0}/dashio/utilities/c64_decode.py +0 -0
  77. {dashio-3.6.2 → dashio-3.7.0}/dashio/utilities/c64_encode.py +0 -0
  78. {dashio-3.6.2 → dashio-3.7.0}/dashio/utilities/dashio_data_exporter.py +0 -0
  79. {dashio-3.6.2 → dashio-3.7.0}/dashio/zeroconf_service.py +0 -0
  80. {dashio-3.6.2 → dashio-3.7.0}/dashio/zmq_connection.py +0 -0
  81. {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/dependency_links.txt +0 -0
  82. {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/entry_points.txt +0 -0
  83. {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/not-zip-safe +0 -0
  84. {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/requires.txt +0 -0
  85. {dashio-3.6.2 → dashio-3.7.0}/dashio.egg-info/top_level.txt +0 -0
  86. {dashio-3.6.2 → dashio-3.7.0}/setup.cfg +0 -0
  87. {dashio-3.6.2 → dashio-3.7.0}/tests/test_alarm.py +0 -0
  88. {dashio-3.6.2 → dashio-3.7.0}/tests/test_button.py +0 -0
  89. {dashio-3.6.2 → dashio-3.7.0}/tests/test_button_group.py +0 -0
  90. {dashio-3.6.2 → dashio-3.7.0}/tests/test_cfg_round_trip.py +0 -0
  91. {dashio-3.6.2 → dashio-3.7.0}/tests/test_chart.py +0 -0
  92. {dashio-3.6.2 → dashio-3.7.0}/tests/test_control.py +0 -0
  93. {dashio-3.6.2 → dashio-3.7.0}/tests/test_dashdevice.py +0 -0
  94. {dashio-3.6.2 → dashio-3.7.0}/tests/test_device_layouts.py +0 -0
  95. {dashio-3.6.2 → dashio-3.7.0}/tests/test_device_view.py +0 -0
  96. {dashio-3.6.2 → dashio-3.7.0}/tests/test_dial.py +0 -0
  97. {dashio-3.6.2 → dashio-3.7.0}/tests/test_direction.py +0 -0
  98. {dashio-3.6.2 → dashio-3.7.0}/tests/test_event_log.py +0 -0
  99. {dashio-3.6.2 → dashio-3.7.0}/tests/test_knob.py +0 -0
  100. {dashio-3.6.2 → dashio-3.7.0}/tests/test_label.py +0 -0
  101. {dashio-3.6.2 → dashio-3.7.0}/tests/test_map.py +0 -0
  102. {dashio-3.6.2 → dashio-3.7.0}/tests/test_menu.py +0 -0
  103. {dashio-3.6.2 → dashio-3.7.0}/tests/test_selector.py +0 -0
  104. {dashio-3.6.2 → dashio-3.7.0}/tests/test_slider.py +0 -0
  105. {dashio-3.6.2 → dashio-3.7.0}/tests/test_tcpconnection.py +0 -0
  106. {dashio-3.6.2 → dashio-3.7.0}/tests/test_textbox.py +0 -0
  107. {dashio-3.6.2 → dashio-3.7.0}/tests/test_time_graph.py +0 -0
  108. {dashio-3.6.2 → dashio-3.7.0}/tests/test_zqmconnection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dashio
3
- Version: 3.6.2
3
+ Version: 3.7.0
4
4
  Summary: DashIO interface library
5
5
  Author-email: James Boulton <james@dashio.com>
6
6
  License-Expression: MIT
@@ -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
- # self.LWD = "OFFLINE"
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
- # self.dash_c.on_log = self.__on_log
309
- # self._dash_c.will_set(self.data_topic, self.LWD, qos=1, retain=False)
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
- """Close the connection."""
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
- handler(*args, **kargs)
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
- has_data = True
152
- if has_data:
153
- data_str += "\n"
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.map_to_json()}"
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 _, value in self.tracks:
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("track_id")
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dashio
3
- Version: 3.6.2
3
+ Version: 3.7.0
4
4
  Summary: DashIO interface library
5
5
  Author-email: James Boulton <james@dashio.com>
6
6
  License-Expression: MIT
@@ -92,6 +92,7 @@ tests/test_device_view.py
92
92
  tests/test_dial.py
93
93
  tests/test_direction.py
94
94
  tests/test_event_log.py
95
+ tests/test_history_replies.py
95
96
  tests/test_knob.py
96
97
  tests/test_label.py
97
98
  tests/test_map.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dashio"
7
- version = "3.6.2"
7
+ version = "3.7.0"
8
8
  description = "DashIO interface library"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "James Boulton", email = "james@dashio.com" }]
@@ -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