dashio 3.3.1__tar.gz → 3.3.2__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.3.2/PKG-INFO +197 -0
- dashio-3.3.2/README.md +181 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/chart.py +36 -3
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/enums.py +0 -1
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/time_graph.py +54 -3
- dashio-3.3.2/dashio.egg-info/PKG-INFO +197 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio.egg-info/SOURCES.txt +23 -1
- {dashio-3.3.1 → dashio-3.3.2}/setup.py +1 -1
- dashio-3.3.2/tests/test_alarm.py +12 -0
- dashio-3.3.2/tests/test_button.py +44 -0
- dashio-3.3.2/tests/test_button_group.py +26 -0
- dashio-3.3.2/tests/test_chart.py +71 -0
- dashio-3.3.2/tests/test_control.py +18 -0
- dashio-3.3.2/tests/test_dashconnection.py +18 -0
- dashio-3.3.2/tests/test_dashdevice.py +30 -0
- dashio-3.3.2/tests/test_device_view.py +77 -0
- dashio-3.3.2/tests/test_dial.py +70 -0
- dashio-3.3.2/tests/test_direction.py +51 -0
- dashio-3.3.2/tests/test_event_log.py +33 -0
- dashio-3.3.2/tests/test_knob.py +65 -0
- dashio-3.3.2/tests/test_label.py +25 -0
- dashio-3.3.2/tests/test_map.py +75 -0
- dashio-3.3.2/tests/test_menu.py +37 -0
- dashio-3.3.2/tests/test_mqttconntection.py +12 -0
- dashio-3.3.2/tests/test_selector.py +26 -0
- dashio-3.3.2/tests/test_slider.py +77 -0
- dashio-3.3.2/tests/test_tcpconnection.py +12 -0
- dashio-3.3.2/tests/test_textbox.py +62 -0
- dashio-3.3.2/tests/test_time_graph.py +46 -0
- dashio-3.3.2/tests/test_zqmconnection.py +0 -0
- dashio-3.3.1/PKG-INFO +0 -924
- dashio-3.3.1/README.md +0 -908
- dashio-3.3.1/dashio.egg-info/PKG-INFO +0 -924
- {dashio-3.3.1 → dashio-3.3.2}/LICENSE +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/__init__.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/action_station.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/action_station_services/__init__.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/action_station_services/action_station_service_config.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/action_station_services/as_servicel.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/action_station_services/clock_servicel.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/action_station_services/modbus_service.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/action_station_services/task_service.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/action_station_services/timer_service.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/bleconnection.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/constants.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/dashconnection.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/device.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/__init__.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/alarm.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/audio_visual_display.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/button.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/button_group.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/color_picker.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/control.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/device_view.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/dial.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/direction.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/event.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/event_log.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/knob.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/label.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/map.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/menu.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/ring_buffer.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/selector.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/slider.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/iotcontrol/textbox.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/ip.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/load_config.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/mqttconnection.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/py.typed +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/serialconnection.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/tcpconnection.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/zeroconf_service.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio/zmqconnection.py +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio.egg-info/dependency_links.txt +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio.egg-info/not-zip-safe +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio.egg-info/requires.txt +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/dashio.egg-info/top_level.txt +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/pyproject.toml +0 -0
- {dashio-3.3.1 → dashio-3.3.2}/setup.cfg +0 -0
dashio-3.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: dashio
|
|
3
|
+
Version: 3.3.2
|
|
4
|
+
Summary: DashIO interface library
|
|
5
|
+
Home-page: https://dashio.io
|
|
6
|
+
Download-URL: https://github.com/dashio-connect/python-dashio
|
|
7
|
+
Author: James Boulton
|
|
8
|
+
Author-email: james@dashio.com
|
|
9
|
+
License: MIT
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >3.6.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Provides-Extra: testing
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
|
|
17
|
+
# DashIO
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
[**DashIO**](http://dashio.io) - It is a quick effortless way to connect your IoT device to your phone, tablet or iPad using the free **Dash** app. It allows easy setup of controls such as Dials, Text Boxes, Charts, Graphs, Notifications..., from your IoT device. You can define the look and layout of the controls on your phone from your IoT device. There are three methods to connect to your phone; Bluetooth Low Energy (BLE), TCP or MQTT.
|
|
22
|
+
|
|
23
|
+
## Getting Started
|
|
24
|
+
|
|
25
|
+
For the big picture on **DashIO**, take a look at our website: <a href="https://dashio.io">dashio.io</a>
|
|
26
|
+
|
|
27
|
+
## Documentation
|
|
28
|
+
|
|
29
|
+
For all documentation and software guides: <a href="https://dashio.io/documents">dashio.io/documents</a>
|
|
30
|
+
|
|
31
|
+
For the **DashIO** Python guide: <a href="https://dashio.io/guide-python">dashio.io/guide-python</a>
|
|
32
|
+
|
|
33
|
+
For the **DashIO** Python library: <a href="https://dashio.io/python-library/">dashio.io/python-library</a>
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
There are plenty of examples in the github repository under the Examples directory.
|
|
38
|
+
|
|
39
|
+
## Dash IoT Application
|
|
40
|
+
|
|
41
|
+
The **Dash** app is free and available for both Apple and Android devices. Use it to create beautiful and powerful user interfaces to you IoT devices.
|
|
42
|
+
|
|
43
|
+
Dashio Phone | Dashio Tablet
|
|
44
|
+
:-------------------------:|:-------------------------:
|
|
45
|
+
<img src="https://dashio.io/wp-content/uploads/2020/11/IMG_4154.jpeg" width="150" /> | <img src="https://dashio.io/wp-content/uploads/2020/12/IMG_4203.jpeg" width="450" />
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
* python3.6 and above
|
|
50
|
+
* paho-mqtt
|
|
51
|
+
* pyzmq
|
|
52
|
+
* python-dateutil
|
|
53
|
+
* zeroconf
|
|
54
|
+
* shortuuid
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
`pip3 install dashio`
|
|
59
|
+
|
|
60
|
+
Or
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
git clone https://github.com/dashio-connect/python-dashio.git
|
|
64
|
+
cd python-dashio
|
|
65
|
+
pip3 install .
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## A Quick Guide
|
|
69
|
+
|
|
70
|
+
This guide covers the **DashIO** python library. For information on the [***Dash***](https://dashio.io/dashboard) phone app please visit the website.
|
|
71
|
+
|
|
72
|
+
### Basics
|
|
73
|
+
|
|
74
|
+
So what is **DashIO**? It is a quick effortless way to connect your IoT device to your phone. It allows easy setup of controls such as Dials, Text Boxes, Maps, Graphs, Notifications..., from your Device. You can define the look and layout of the controls on your phone from your IoT device. There are three methods to connect to your phone tcp, mqtt, dash, and BLE. What's Dash then? Dash is a mqtt server with extra bits added in to allow you to send notifications, share your devices, and save your settings from your phone via the **Dash** app.
|
|
75
|
+
|
|
76
|
+
Show me some code.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Examples/ex01.py
|
|
80
|
+
import dashio
|
|
81
|
+
import random
|
|
82
|
+
import time
|
|
83
|
+
|
|
84
|
+
device = dashio.Device("aDeviceType", "aDeviceID", "aDeviceName")
|
|
85
|
+
tcp_con = dashio.TCPConnection()
|
|
86
|
+
tcp_con.add_device(device)
|
|
87
|
+
first_dial_control = dashio.Dial("FirstDial")
|
|
88
|
+
device.add_control(first_dial_control)
|
|
89
|
+
|
|
90
|
+
while True:
|
|
91
|
+
first_dial_control.dial_value = random.random() * 100
|
|
92
|
+
time.sleep(5)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This is about the fewest lines of code to get talking to the app. There is a lot happening under the hood to make this work. After the import we create a device with three attributes:
|
|
96
|
+
|
|
97
|
+
* "aDeviceType": a common name device_type for all IoT devices using this code which is used for device discovery
|
|
98
|
+
* "aDeviceID": a device_ID to uniquely identify this device, preferably a UUID.
|
|
99
|
+
* "aDeviceName": The name of this device, which can be changed at any time.
|
|
100
|
+
|
|
101
|
+
These attributes describe the device to the app and allow you to distinguish one of your devices from another.
|
|
102
|
+
|
|
103
|
+
The next two lines create a TCP connection and then add the device to the connection. This device is discoverable by the Dash app. You can also discover your IoT device using a third party Bonjour/Zeroconf discovery tool. The mDNS service will be "DashIO.tcp."
|
|
104
|
+
|
|
105
|
+
Though this device is discoverable by the app it would be nice to have the DashIO app automatically setup a new DeviceView and place your control on the new DeviceView. To do that we need to add a few more lines of code:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# Examples/ex02.py
|
|
109
|
+
import dashio
|
|
110
|
+
import random
|
|
111
|
+
import time
|
|
112
|
+
|
|
113
|
+
device = dashio.Device("aDeviceType", "aDeviceID", "aDeviceName")
|
|
114
|
+
tcp_con = dashio.TCPConnection()
|
|
115
|
+
tcp_con.add_device(device)
|
|
116
|
+
first_dial_control = dashio.Dial("FirstDial", control_position=dashio.ControlPosition(0.24, 0.36, 0.54, 0.26))
|
|
117
|
+
device.add_control(first_dial_control)
|
|
118
|
+
|
|
119
|
+
dv_dial = dashio.DeviceView("aDeviceViewID", "A Dial")
|
|
120
|
+
dv_dial.add_control(first_dial_control)
|
|
121
|
+
device.add_control(dv_dial)
|
|
122
|
+
|
|
123
|
+
while True:
|
|
124
|
+
first_dial_control.dial_value = random.random() * 100
|
|
125
|
+
time.sleep(5)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
First we altered the instantiation of a Dial by including a control_position. This allows us to place the control at a set location. The added lines instantiated a DeviceView control, which we than added the dial control. Finally we added the DeviceView to the device.
|
|
129
|
+
|
|
130
|
+
The next piece of the puzzle to consider is how do we get data from the DashIO app? Lets add a Knob and connect it to the Dial:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
# Examples/ex03.py
|
|
134
|
+
import dashio
|
|
135
|
+
import time
|
|
136
|
+
|
|
137
|
+
device = dashio.Device("aDeviceType", "aDeviceID", "aDeviceName")
|
|
138
|
+
tcp_con = dashio.TCPConnection()
|
|
139
|
+
tcp_con.add_device(device)
|
|
140
|
+
first_dial_control = dashio.Dial("FirstDial", control_position=dashio.ControlPosition(0.24, 0.36, 0.54, 0.26))
|
|
141
|
+
device.add_control(first_dial_control)
|
|
142
|
+
|
|
143
|
+
dv = dashio.DeviceView("aDeviceViewID", "A Dial")
|
|
144
|
+
dv.add_control(first_dial_control)
|
|
145
|
+
device.add_control(dv)
|
|
146
|
+
|
|
147
|
+
def knob_event_handler(msg):
|
|
148
|
+
first_dial_control.dial_value = float(msg[3])
|
|
149
|
+
|
|
150
|
+
aknob = dashio.Knob("aKNB", control_position=dashio.ControlPosition(0.24, 0.14, 0.54, 0.26))
|
|
151
|
+
aknob.add_receive_message_callback += knob_event_handler
|
|
152
|
+
dv.add_control(aknob)
|
|
153
|
+
device.add_control(aknob)
|
|
154
|
+
|
|
155
|
+
while True:
|
|
156
|
+
time.sleep(1)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
First we added a function that sets the dial value. Next we added a Knob control and added our new function to be called when it receives data from the DashIO app. We also add it to the DeviceView and to the device. Now when the knob in the DashIO app is moved the dial is set to the same value.
|
|
160
|
+
|
|
161
|
+
### Using the Config64
|
|
162
|
+
|
|
163
|
+
The **Dash** app can generate a CFG64 text string that defines the controls, the controls layout, and device parameters for the Device. The CFG64 string can be used in a number of ways. If we run the example above and using the **Dash** app to export a layout we can use the layout to setup the device and controls.
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
# Examples/ex04.py
|
|
167
|
+
import dashio
|
|
168
|
+
import time
|
|
169
|
+
|
|
170
|
+
cfg64 = "pVNdr5pAEP0rN/tMjHCrbXnjQ60RxQD1Nmn6wIVVN8IuXZar1vjfOwuLeNWmTfo2OztzZnLmnBPybA+Z339oaLbwbYhOqBTHDCMT"\
|
|
171
|
+
"LfxgbnlIQwmjgrNs6kIyni1sSJWYpj7Njj4NcIbjEuoFr7CG8viATL3f11ARc0xF0+TiN5LgFcF7eGuI43QVZxU0fRxoSBBRz7Oe"\
|
|
172
|
+
"ZpS9wvcWk81WBLEgDJn9njFUNUtWEshRqLX9KPLncpEt288Jncux6zgrYYVjW3dB0D9oaAfYDssYh+4Ap9Cakjgbsyxj+7Ie3PbL"\
|
|
173
|
+
"fFtpyy01tCep2F7gBgB3uBtiQDYnsFz/DGyGnhs0vEajb5GKnCUyT6hgXCBzMBwASaSw0pTjsoRZ+mejpw8/9fSebvTRWUPuVEHM"\
|
|
174
|
+
"rWUTeP5EZUaLr000CZZfmshauU0Qek6kOh1PBe5q9fLuurYVTh15XMEzYG8MNw7JL/gxYK0NJylQUOUUFjMMSTNcs8m0l6ZVfinR"\
|
|
175
|
+
"b0Ryc285w2Y8xbzl9WVLBH4Kc7bD7f9uQ9OO9jjZqY+/tEQ8pmWtteQI3MtF3mN08nLhsKo1kkmbHR4NbP/ukCUrAagFmc/GVWlH"\
|
|
176
|
+
"nQ5KJUDEIs7lxPBnBd1IysGdWl5Nf8FxQspaxIDYHmM5Hd34bEx4KdTC/+UphfHAU7X+SXYhuHHFH512b6tnACkYoaK7q/ILaOMV"\
|
|
177
|
+
"8ysUbzSOHrv1X61VUSKkS9C1y6JO/XYULdpoouzijCfScGnNVIhFVQAAheNoe7ImmkiKZteOSqXl9SbAbxCez78B"
|
|
178
|
+
|
|
179
|
+
config_dict = dashio.decode_cfg64(cfg64)
|
|
180
|
+
device = dashio.Device("aDeviceType", "aDeviceID", "aDeviceName", cfg_dict=config_dict)
|
|
181
|
+
tcp_con = dashio.TCPConnection()
|
|
182
|
+
tcp_con.add_device(device)
|
|
183
|
+
|
|
184
|
+
aknob: dashio.Knob = device.get_control(dashio.ControlName.KNOB, "aKNB")
|
|
185
|
+
first_dial_control: dashio.Dial = device.get_control(dashio.ControlName.DIAL, "FirstDial")
|
|
186
|
+
|
|
187
|
+
def knob_event_handler(msg):
|
|
188
|
+
aknob.knob_dial_value = float(msg[3])
|
|
189
|
+
first_dial_control.dial_value = float(msg[3])
|
|
190
|
+
|
|
191
|
+
aknob.add_receive_message_callback(knob_event_handler)
|
|
192
|
+
|
|
193
|
+
while True:
|
|
194
|
+
time.sleep(1)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
We've added the cfg64 string. Then decoded it with *dashio.decode_cfg64(cfg64)*. This function returns a dictionary that we can pass into Device so that it can to instantiate and add the controls.
|
dashio-3.3.2/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# DashIO
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
[**DashIO**](http://dashio.io) - It is a quick effortless way to connect your IoT device to your phone, tablet or iPad using the free **Dash** app. It allows easy setup of controls such as Dials, Text Boxes, Charts, Graphs, Notifications..., from your IoT device. You can define the look and layout of the controls on your phone from your IoT device. There are three methods to connect to your phone; Bluetooth Low Energy (BLE), TCP or MQTT.
|
|
6
|
+
|
|
7
|
+
## Getting Started
|
|
8
|
+
|
|
9
|
+
For the big picture on **DashIO**, take a look at our website: <a href="https://dashio.io">dashio.io</a>
|
|
10
|
+
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
For all documentation and software guides: <a href="https://dashio.io/documents">dashio.io/documents</a>
|
|
14
|
+
|
|
15
|
+
For the **DashIO** Python guide: <a href="https://dashio.io/guide-python">dashio.io/guide-python</a>
|
|
16
|
+
|
|
17
|
+
For the **DashIO** Python library: <a href="https://dashio.io/python-library/">dashio.io/python-library</a>
|
|
18
|
+
|
|
19
|
+
## Examples
|
|
20
|
+
|
|
21
|
+
There are plenty of examples in the github repository under the Examples directory.
|
|
22
|
+
|
|
23
|
+
## Dash IoT Application
|
|
24
|
+
|
|
25
|
+
The **Dash** app is free and available for both Apple and Android devices. Use it to create beautiful and powerful user interfaces to you IoT devices.
|
|
26
|
+
|
|
27
|
+
Dashio Phone | Dashio Tablet
|
|
28
|
+
:-------------------------:|:-------------------------:
|
|
29
|
+
<img src="https://dashio.io/wp-content/uploads/2020/11/IMG_4154.jpeg" width="150" /> | <img src="https://dashio.io/wp-content/uploads/2020/12/IMG_4203.jpeg" width="450" />
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
* python3.6 and above
|
|
34
|
+
* paho-mqtt
|
|
35
|
+
* pyzmq
|
|
36
|
+
* python-dateutil
|
|
37
|
+
* zeroconf
|
|
38
|
+
* shortuuid
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
`pip3 install dashio`
|
|
43
|
+
|
|
44
|
+
Or
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
git clone https://github.com/dashio-connect/python-dashio.git
|
|
48
|
+
cd python-dashio
|
|
49
|
+
pip3 install .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## A Quick Guide
|
|
53
|
+
|
|
54
|
+
This guide covers the **DashIO** python library. For information on the [***Dash***](https://dashio.io/dashboard) phone app please visit the website.
|
|
55
|
+
|
|
56
|
+
### Basics
|
|
57
|
+
|
|
58
|
+
So what is **DashIO**? It is a quick effortless way to connect your IoT device to your phone. It allows easy setup of controls such as Dials, Text Boxes, Maps, Graphs, Notifications..., from your Device. You can define the look and layout of the controls on your phone from your IoT device. There are three methods to connect to your phone tcp, mqtt, dash, and BLE. What's Dash then? Dash is a mqtt server with extra bits added in to allow you to send notifications, share your devices, and save your settings from your phone via the **Dash** app.
|
|
59
|
+
|
|
60
|
+
Show me some code.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# Examples/ex01.py
|
|
64
|
+
import dashio
|
|
65
|
+
import random
|
|
66
|
+
import time
|
|
67
|
+
|
|
68
|
+
device = dashio.Device("aDeviceType", "aDeviceID", "aDeviceName")
|
|
69
|
+
tcp_con = dashio.TCPConnection()
|
|
70
|
+
tcp_con.add_device(device)
|
|
71
|
+
first_dial_control = dashio.Dial("FirstDial")
|
|
72
|
+
device.add_control(first_dial_control)
|
|
73
|
+
|
|
74
|
+
while True:
|
|
75
|
+
first_dial_control.dial_value = random.random() * 100
|
|
76
|
+
time.sleep(5)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This is about the fewest lines of code to get talking to the app. There is a lot happening under the hood to make this work. After the import we create a device with three attributes:
|
|
80
|
+
|
|
81
|
+
* "aDeviceType": a common name device_type for all IoT devices using this code which is used for device discovery
|
|
82
|
+
* "aDeviceID": a device_ID to uniquely identify this device, preferably a UUID.
|
|
83
|
+
* "aDeviceName": The name of this device, which can be changed at any time.
|
|
84
|
+
|
|
85
|
+
These attributes describe the device to the app and allow you to distinguish one of your devices from another.
|
|
86
|
+
|
|
87
|
+
The next two lines create a TCP connection and then add the device to the connection. This device is discoverable by the Dash app. You can also discover your IoT device using a third party Bonjour/Zeroconf discovery tool. The mDNS service will be "DashIO.tcp."
|
|
88
|
+
|
|
89
|
+
Though this device is discoverable by the app it would be nice to have the DashIO app automatically setup a new DeviceView and place your control on the new DeviceView. To do that we need to add a few more lines of code:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# Examples/ex02.py
|
|
93
|
+
import dashio
|
|
94
|
+
import random
|
|
95
|
+
import time
|
|
96
|
+
|
|
97
|
+
device = dashio.Device("aDeviceType", "aDeviceID", "aDeviceName")
|
|
98
|
+
tcp_con = dashio.TCPConnection()
|
|
99
|
+
tcp_con.add_device(device)
|
|
100
|
+
first_dial_control = dashio.Dial("FirstDial", control_position=dashio.ControlPosition(0.24, 0.36, 0.54, 0.26))
|
|
101
|
+
device.add_control(first_dial_control)
|
|
102
|
+
|
|
103
|
+
dv_dial = dashio.DeviceView("aDeviceViewID", "A Dial")
|
|
104
|
+
dv_dial.add_control(first_dial_control)
|
|
105
|
+
device.add_control(dv_dial)
|
|
106
|
+
|
|
107
|
+
while True:
|
|
108
|
+
first_dial_control.dial_value = random.random() * 100
|
|
109
|
+
time.sleep(5)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
First we altered the instantiation of a Dial by including a control_position. This allows us to place the control at a set location. The added lines instantiated a DeviceView control, which we than added the dial control. Finally we added the DeviceView to the device.
|
|
113
|
+
|
|
114
|
+
The next piece of the puzzle to consider is how do we get data from the DashIO app? Lets add a Knob and connect it to the Dial:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# Examples/ex03.py
|
|
118
|
+
import dashio
|
|
119
|
+
import time
|
|
120
|
+
|
|
121
|
+
device = dashio.Device("aDeviceType", "aDeviceID", "aDeviceName")
|
|
122
|
+
tcp_con = dashio.TCPConnection()
|
|
123
|
+
tcp_con.add_device(device)
|
|
124
|
+
first_dial_control = dashio.Dial("FirstDial", control_position=dashio.ControlPosition(0.24, 0.36, 0.54, 0.26))
|
|
125
|
+
device.add_control(first_dial_control)
|
|
126
|
+
|
|
127
|
+
dv = dashio.DeviceView("aDeviceViewID", "A Dial")
|
|
128
|
+
dv.add_control(first_dial_control)
|
|
129
|
+
device.add_control(dv)
|
|
130
|
+
|
|
131
|
+
def knob_event_handler(msg):
|
|
132
|
+
first_dial_control.dial_value = float(msg[3])
|
|
133
|
+
|
|
134
|
+
aknob = dashio.Knob("aKNB", control_position=dashio.ControlPosition(0.24, 0.14, 0.54, 0.26))
|
|
135
|
+
aknob.add_receive_message_callback += knob_event_handler
|
|
136
|
+
dv.add_control(aknob)
|
|
137
|
+
device.add_control(aknob)
|
|
138
|
+
|
|
139
|
+
while True:
|
|
140
|
+
time.sleep(1)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
First we added a function that sets the dial value. Next we added a Knob control and added our new function to be called when it receives data from the DashIO app. We also add it to the DeviceView and to the device. Now when the knob in the DashIO app is moved the dial is set to the same value.
|
|
144
|
+
|
|
145
|
+
### Using the Config64
|
|
146
|
+
|
|
147
|
+
The **Dash** app can generate a CFG64 text string that defines the controls, the controls layout, and device parameters for the Device. The CFG64 string can be used in a number of ways. If we run the example above and using the **Dash** app to export a layout we can use the layout to setup the device and controls.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# Examples/ex04.py
|
|
151
|
+
import dashio
|
|
152
|
+
import time
|
|
153
|
+
|
|
154
|
+
cfg64 = "pVNdr5pAEP0rN/tMjHCrbXnjQ60RxQD1Nmn6wIVVN8IuXZar1vjfOwuLeNWmTfo2OztzZnLmnBPybA+Z339oaLbwbYhOqBTHDCMT"\
|
|
155
|
+
"LfxgbnlIQwmjgrNs6kIyni1sSJWYpj7Njj4NcIbjEuoFr7CG8viATL3f11ARc0xF0+TiN5LgFcF7eGuI43QVZxU0fRxoSBBRz7Oe"\
|
|
156
|
+
"ZpS9wvcWk81WBLEgDJn9njFUNUtWEshRqLX9KPLncpEt288Jncux6zgrYYVjW3dB0D9oaAfYDssYh+4Ap9Cakjgbsyxj+7Ie3PbL"\
|
|
157
|
+
"fFtpyy01tCep2F7gBgB3uBtiQDYnsFz/DGyGnhs0vEajb5GKnCUyT6hgXCBzMBwASaSw0pTjsoRZ+mejpw8/9fSebvTRWUPuVEHM"\
|
|
158
|
+
"rWUTeP5EZUaLr000CZZfmshauU0Qek6kOh1PBe5q9fLuurYVTh15XMEzYG8MNw7JL/gxYK0NJylQUOUUFjMMSTNcs8m0l6ZVfinR"\
|
|
159
|
+
"b0Ryc285w2Y8xbzl9WVLBH4Kc7bD7f9uQ9OO9jjZqY+/tEQ8pmWtteQI3MtF3mN08nLhsKo1kkmbHR4NbP/ukCUrAagFmc/GVWlH"\
|
|
160
|
+
"nQ5KJUDEIs7lxPBnBd1IysGdWl5Nf8FxQspaxIDYHmM5Hd34bEx4KdTC/+UphfHAU7X+SXYhuHHFH512b6tnACkYoaK7q/ILaOMV"\
|
|
161
|
+
"8ysUbzSOHrv1X61VUSKkS9C1y6JO/XYULdpoouzijCfScGnNVIhFVQAAheNoe7ImmkiKZteOSqXl9SbAbxCez78B"
|
|
162
|
+
|
|
163
|
+
config_dict = dashio.decode_cfg64(cfg64)
|
|
164
|
+
device = dashio.Device("aDeviceType", "aDeviceID", "aDeviceName", cfg_dict=config_dict)
|
|
165
|
+
tcp_con = dashio.TCPConnection()
|
|
166
|
+
tcp_con.add_device(device)
|
|
167
|
+
|
|
168
|
+
aknob: dashio.Knob = device.get_control(dashio.ControlName.KNOB, "aKNB")
|
|
169
|
+
first_dial_control: dashio.Dial = device.get_control(dashio.ControlName.DIAL, "FirstDial")
|
|
170
|
+
|
|
171
|
+
def knob_event_handler(msg):
|
|
172
|
+
aknob.knob_dial_value = float(msg[3])
|
|
173
|
+
first_dial_control.dial_value = float(msg[3])
|
|
174
|
+
|
|
175
|
+
aknob.add_receive_message_callback(knob_event_handler)
|
|
176
|
+
|
|
177
|
+
while True:
|
|
178
|
+
time.sleep(1)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
We've added the cfg64 string. Then decoded it with *dashio.decode_cfg64(cfg64)*. This function returns a dictionary that we can pass into Device so that it can to instantiate and add the controls.
|
|
@@ -29,11 +29,14 @@ from .enums import Color, ChartLineType, ChartXAxisLabelsStyle, TitlePosition
|
|
|
29
29
|
class ChartLine:
|
|
30
30
|
"""ChartLine class
|
|
31
31
|
"""
|
|
32
|
-
def __init__(self, name="", line_type=ChartLineType.LINE, color=Color.BLACK):
|
|
32
|
+
def __init__(self, name="", line_type=ChartLineType.LINE, color=Color.BLACK, right_axis=False):
|
|
33
33
|
self.name = name.translate(BAD_CHARS)
|
|
34
34
|
self.line_type = line_type
|
|
35
35
|
self.color = color
|
|
36
36
|
self.data = []
|
|
37
|
+
self.axis_side = 'left'
|
|
38
|
+
if right_axis:
|
|
39
|
+
self.axis_side = 'right'
|
|
37
40
|
|
|
38
41
|
def get_line_data(self):
|
|
39
42
|
"""Returns the line data formated for the iotdashboard app
|
|
@@ -43,7 +46,7 @@ class ChartLine:
|
|
|
43
46
|
str
|
|
44
47
|
The formatted line data
|
|
45
48
|
"""
|
|
46
|
-
data_str = f"\t{self.name}\t{self.line_type.value}\t{self.color.value}\t"
|
|
49
|
+
data_str = f"\t{self.name}\t{self.line_type.value}\t{self.color.value}\t{self.axis_side}\t"
|
|
47
50
|
data_str += "\t".join(map(str, self.data))
|
|
48
51
|
data_str += "\n"
|
|
49
52
|
return data_str
|
|
@@ -65,6 +68,9 @@ class ChartConfig(ControlConfig):
|
|
|
65
68
|
y_axis_min: float,
|
|
66
69
|
y_axis_max: float,
|
|
67
70
|
y_axis_num_bars: int,
|
|
71
|
+
y_axis_label_rt: str,
|
|
72
|
+
y_axis_min_rt: float,
|
|
73
|
+
y_axis_max_rt: float,
|
|
68
74
|
control_position: ControlPosition,
|
|
69
75
|
) -> None:
|
|
70
76
|
super().__init__(control_id, title, control_position, title_position)
|
|
@@ -77,6 +83,9 @@ class ChartConfig(ControlConfig):
|
|
|
77
83
|
self.cfg["yAxisMin"] = y_axis_min
|
|
78
84
|
self.cfg["yAxisMax"] = y_axis_max
|
|
79
85
|
self.cfg["yAxisNumBars"] = y_axis_num_bars
|
|
86
|
+
self.cfg["yAxisLabelRt"] = y_axis_label_rt.translate(BAD_CHARS)
|
|
87
|
+
self.cfg["yAxisMinRt"] = y_axis_min_rt
|
|
88
|
+
self.cfg["yAxisMaxRt"] = y_axis_max_rt
|
|
80
89
|
|
|
81
90
|
@classmethod
|
|
82
91
|
def from_dict(cls, cfg_dict: dict):
|
|
@@ -104,6 +113,9 @@ class ChartConfig(ControlConfig):
|
|
|
104
113
|
cfg_dict["yAxisMin"],
|
|
105
114
|
cfg_dict["yAxisMax"],
|
|
106
115
|
cfg_dict["yAxisNumBars"],
|
|
116
|
+
cfg_dict["yAxisLabelRt"],
|
|
117
|
+
cfg_dict["yAxisMinRt"],
|
|
118
|
+
cfg_dict["yAxisMaxRt"],
|
|
107
119
|
ControlPosition(cfg_dict["xPositionRatio"], cfg_dict["yPositionRatio"], cfg_dict["widthRatio"], cfg_dict["heightRatio"])
|
|
108
120
|
)
|
|
109
121
|
tmp_cls.parent_id = cfg_dict["parentID"]
|
|
@@ -138,8 +150,11 @@ class Chart(Control):
|
|
|
138
150
|
x_axis_labels_style=ChartXAxisLabelsStyle.ON,
|
|
139
151
|
y_axis_label="",
|
|
140
152
|
y_axis_min=0.0,
|
|
141
|
-
y_axis_max=
|
|
153
|
+
y_axis_max=1000.0,
|
|
142
154
|
y_axis_num_bars=5,
|
|
155
|
+
y_axis_label_rt="",
|
|
156
|
+
y_axis_min_rt=0.0,
|
|
157
|
+
y_axis_max_rt=1000.0,
|
|
143
158
|
control_position=None,
|
|
144
159
|
):
|
|
145
160
|
"""A Chart Control
|
|
@@ -188,6 +203,9 @@ class Chart(Control):
|
|
|
188
203
|
y_axis_min,
|
|
189
204
|
y_axis_max,
|
|
190
205
|
y_axis_num_bars,
|
|
206
|
+
y_axis_label_rt,
|
|
207
|
+
y_axis_min_rt,
|
|
208
|
+
y_axis_max_rt,
|
|
191
209
|
control_position
|
|
192
210
|
)
|
|
193
211
|
)
|
|
@@ -198,6 +216,8 @@ class Chart(Control):
|
|
|
198
216
|
self._y_axis_min = y_axis_min
|
|
199
217
|
self._y_axis_max = y_axis_max
|
|
200
218
|
self._y_axis_num_bars = y_axis_num_bars
|
|
219
|
+
self._y_axis_min_rt = y_axis_min_rt
|
|
220
|
+
self._y_axis_max_rt = y_axis_max_rt
|
|
201
221
|
|
|
202
222
|
@property
|
|
203
223
|
def x_axis_min(self):
|
|
@@ -224,6 +244,16 @@ class Chart(Control):
|
|
|
224
244
|
"""Returns the Y axis maximum value"""
|
|
225
245
|
return self._y_axis_max
|
|
226
246
|
|
|
247
|
+
@property
|
|
248
|
+
def y_axis_min_rt(self):
|
|
249
|
+
"""Returns the Y axis minimum value right side"""
|
|
250
|
+
return self._y_axis_min_rt
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def y_axis_max_rt(self):
|
|
254
|
+
"""Returns the Y axis maximum value right side"""
|
|
255
|
+
return self._y_axis_max_rt
|
|
256
|
+
|
|
227
257
|
@property
|
|
228
258
|
def y_axis_num_bars(self):
|
|
229
259
|
"""Returns the y axis number of bars"""
|
|
@@ -255,6 +285,9 @@ class Chart(Control):
|
|
|
255
285
|
cfg_dict["yAxisMin"],
|
|
256
286
|
cfg_dict["yAxisMax"],
|
|
257
287
|
cfg_dict["yAxisNumBars"],
|
|
288
|
+
cfg_dict.get("yAxisLabelRt", ''),
|
|
289
|
+
cfg_dict.get("yAxisMinRt", 0.0),
|
|
290
|
+
cfg_dict.get("yAxisMaxRt", 1000.0),
|
|
258
291
|
ControlPosition(cfg_dict["xPositionRatio"], cfg_dict["yPositionRatio"], cfg_dict["widthRatio"], cfg_dict["heightRatio"])
|
|
259
292
|
)
|
|
260
293
|
tmp_cls.parent_id = cfg_dict["parentID"]
|
|
@@ -57,7 +57,13 @@ class TimeGraphLine:
|
|
|
57
57
|
"""A TimeGraphLine for a TimeGraph control
|
|
58
58
|
"""
|
|
59
59
|
def __init__(
|
|
60
|
-
self,
|
|
60
|
+
self,
|
|
61
|
+
name: str = "",
|
|
62
|
+
line_type: TimeGraphLineType = TimeGraphLineType.LINE,
|
|
63
|
+
color: Color = Color.BLACK,
|
|
64
|
+
max_data_points: int = 60,
|
|
65
|
+
break_data: bool = False,
|
|
66
|
+
right_axis: bool = False
|
|
61
67
|
):
|
|
62
68
|
"""A TimeGraphLine for a TimeGraph control
|
|
63
69
|
|
|
@@ -79,6 +85,12 @@ class TimeGraphLine:
|
|
|
79
85
|
self.color = color
|
|
80
86
|
self.break_data = break_data
|
|
81
87
|
self.data = RingBuffer(max_data_points)
|
|
88
|
+
self.axis_side = 'left'
|
|
89
|
+
if right_axis:
|
|
90
|
+
self.axis_side = 'right'
|
|
91
|
+
|
|
92
|
+
def get_line_format(self):
|
|
93
|
+
return f"\t{self.name}\t{self.line_type.value}\t{self.color.value}\t{self.axis_side}\n"
|
|
82
94
|
|
|
83
95
|
def get_line_from_timestamp(self, timestamp: str) -> str:
|
|
84
96
|
"""Converts data from timestamp to a string formatted for the iotdashboard app
|
|
@@ -88,7 +100,8 @@ class TimeGraphLine:
|
|
|
88
100
|
str
|
|
89
101
|
Formatted line data
|
|
90
102
|
"""
|
|
91
|
-
data_str = f"\t{self.name}\t{self.line_type.value}\t{self.color.value}"
|
|
103
|
+
data_str = f"\t{self.name}\t{self.line_type.value}\t{self.color.value}\t{self.axis_side}"
|
|
104
|
+
|
|
92
105
|
try:
|
|
93
106
|
d_stamp = dateutil.parser.isoparse(timestamp)
|
|
94
107
|
except ValueError:
|
|
@@ -144,6 +157,9 @@ class TimeGraphConfig(ControlConfig):
|
|
|
144
157
|
y_axis_min: float,
|
|
145
158
|
y_axis_max: float,
|
|
146
159
|
y_axis_num_bars: int,
|
|
160
|
+
y_axis_label_rt: str,
|
|
161
|
+
y_axis_min_rt: float,
|
|
162
|
+
y_axis_max_rt: float,
|
|
147
163
|
control_position: ControlPosition
|
|
148
164
|
) -> None:
|
|
149
165
|
super().__init__(control_id, title, control_position, title_position)
|
|
@@ -151,6 +167,9 @@ class TimeGraphConfig(ControlConfig):
|
|
|
151
167
|
self.cfg["yAxisMin"] = y_axis_min
|
|
152
168
|
self.cfg["yAxisMax"] = y_axis_max
|
|
153
169
|
self.cfg["yAxisNumBars"] = y_axis_num_bars
|
|
170
|
+
self.cfg["yAxisLabelRt"] = y_axis_label_rt.translate(BAD_CHARS)
|
|
171
|
+
self.cfg["yAxisMinRt"] = y_axis_min_rt
|
|
172
|
+
self.cfg["yAxisMaxRt"] = y_axis_max_rt
|
|
154
173
|
|
|
155
174
|
@classmethod
|
|
156
175
|
def from_dict(cls, cfg_dict: dict):
|
|
@@ -173,6 +192,9 @@ class TimeGraphConfig(ControlConfig):
|
|
|
173
192
|
cfg_dict["yAxisMin"],
|
|
174
193
|
cfg_dict["yAxisMax"],
|
|
175
194
|
cfg_dict["yAxisNumBars"],
|
|
195
|
+
cfg_dict["yAxisLabelRt"],
|
|
196
|
+
cfg_dict["yAxisMinRt"],
|
|
197
|
+
cfg_dict["yAxisMaxRt"],
|
|
176
198
|
ControlPosition(cfg_dict["xPositionRatio"], cfg_dict["yPositionRatio"], cfg_dict["widthRatio"], cfg_dict["heightRatio"])
|
|
177
199
|
)
|
|
178
200
|
tmp_cls.parent_id = cfg_dict["parentID"]
|
|
@@ -190,8 +212,11 @@ class TimeGraph(Control):
|
|
|
190
212
|
title_position=TitlePosition.BOTTOM,
|
|
191
213
|
y_axis_label="Y axis",
|
|
192
214
|
y_axis_min=0.0,
|
|
193
|
-
y_axis_max=
|
|
215
|
+
y_axis_max=1000.0,
|
|
194
216
|
y_axis_num_bars=5,
|
|
217
|
+
y_axis_label_rt='',
|
|
218
|
+
y_axis_min_rt=0.0,
|
|
219
|
+
y_axis_max_rt=1000.0,
|
|
195
220
|
control_position=None,
|
|
196
221
|
):
|
|
197
222
|
"""A Timegraph control
|
|
@@ -225,6 +250,9 @@ class TimeGraph(Control):
|
|
|
225
250
|
y_axis_min,
|
|
226
251
|
y_axis_max,
|
|
227
252
|
y_axis_num_bars,
|
|
253
|
+
y_axis_label_rt,
|
|
254
|
+
y_axis_min_rt,
|
|
255
|
+
y_axis_max_rt,
|
|
228
256
|
control_position
|
|
229
257
|
)
|
|
230
258
|
)
|
|
@@ -233,6 +261,8 @@ class TimeGraph(Control):
|
|
|
233
261
|
self._y_axis_min = y_axis_min
|
|
234
262
|
self._y_axis_max = y_axis_max
|
|
235
263
|
self._y_axis_num_bars = y_axis_num_bars
|
|
264
|
+
self._y_axis_min_rt = y_axis_min_rt
|
|
265
|
+
self._y_axis_max_rt = y_axis_max_rt
|
|
236
266
|
|
|
237
267
|
self.line_dict = {}
|
|
238
268
|
|
|
@@ -246,6 +276,16 @@ class TimeGraph(Control):
|
|
|
246
276
|
"""Returns the Y axis maximum value"""
|
|
247
277
|
return self._y_axis_max
|
|
248
278
|
|
|
279
|
+
@property
|
|
280
|
+
def y_axis_min_rt(self):
|
|
281
|
+
"""Returns the Y axis minimum value"""
|
|
282
|
+
return self._y_axis_min_rt
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def y_axis_max_rt(self):
|
|
286
|
+
"""Returns the Y axis maximum value"""
|
|
287
|
+
return self._y_axis_max_rt
|
|
288
|
+
|
|
249
289
|
@property
|
|
250
290
|
def y_axis_num_bars(self):
|
|
251
291
|
"""Returns the y axis number of bars"""
|
|
@@ -272,11 +312,22 @@ class TimeGraph(Control):
|
|
|
272
312
|
cfg_dict["yAxisMin"],
|
|
273
313
|
cfg_dict["yAxisMax"],
|
|
274
314
|
cfg_dict["yAxisNumBars"],
|
|
315
|
+
cfg_dict.get("yAxisLabelRt", ''),
|
|
316
|
+
cfg_dict.get("yAxisMinRt", 0.0),
|
|
317
|
+
cfg_dict.get("yAxisMaxRt", 1000.0),
|
|
275
318
|
ControlPosition(cfg_dict["xPositionRatio"], cfg_dict["yPositionRatio"], cfg_dict["widthRatio"], cfg_dict["heightRatio"])
|
|
276
319
|
)
|
|
277
320
|
tmp_cls.parent_id = cfg_dict["parentID"]
|
|
278
321
|
return tmp_cls
|
|
279
322
|
|
|
323
|
+
def get_state(self):
|
|
324
|
+
state_str = ""
|
|
325
|
+
for key, line in self.line_dict.items():
|
|
326
|
+
line_format = line.get_line_format()
|
|
327
|
+
if line_format:
|
|
328
|
+
state_str += self._control_hdr_str + "BRDCST\t" + key + line_format
|
|
329
|
+
return state_str
|
|
330
|
+
|
|
280
331
|
def add_line(self, line_id: str, gline: TimeGraphLine):
|
|
281
332
|
"""Add a line to the TimeGraph
|
|
282
333
|
|