tinytoolslib 0.2.5__py3-none-any.whl
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.
- tinytoolslib/__init__.py +1 -0
- tinytoolslib/__version__.py +1 -0
- tinytoolslib/constants.py +14 -0
- tinytoolslib/discovery.py +167 -0
- tinytoolslib/exceptions.py +49 -0
- tinytoolslib/flash.py +316 -0
- tinytoolslib/models.py +1674 -0
- tinytoolslib/parsers.py +86 -0
- tinytoolslib/requests.py +268 -0
- tinytoolslib-0.2.5.dist-info/METADATA +76 -0
- tinytoolslib-0.2.5.dist-info/RECORD +13 -0
- tinytoolslib-0.2.5.dist-info/WHEEL +5 -0
- tinytoolslib-0.2.5.dist-info/top_level.txt +1 -0
tinytoolslib/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .__version__ import __version__
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.2.5'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Constants related to tinycontrol devices."""
|
|
2
|
+
|
|
3
|
+
# Discovery related stuff
|
|
4
|
+
LK_UDP_PORT = 30403
|
|
5
|
+
LK_UDP_BOOTLOADER_MSG = b"\x12\xf4\x81"
|
|
6
|
+
LK_UDP_DISCOVERY_MSG = b"Discovery: Who is out there?"
|
|
7
|
+
|
|
8
|
+
# Device families. PS and DCDC are pretty much the same as LK (UI differs)
|
|
9
|
+
FAMILY_LK = "LK"
|
|
10
|
+
FAMILY_PS = "PS" # Power socket
|
|
11
|
+
FAMILY_DCDC = "DCDC" # Converter DC/DC
|
|
12
|
+
FAMILY_TCPDU = "tcPDU"
|
|
13
|
+
|
|
14
|
+
FW_URL_TEMPLATE = "https://tinycontrol.pl/firmware/{}/latest/"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import logging
|
|
3
|
+
import socket
|
|
4
|
+
import socketserver
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import netifaces
|
|
9
|
+
|
|
10
|
+
from tinytoolslib.constants import LK_UDP_DISCOVERY_MSG, LK_UDP_PORT
|
|
11
|
+
from tinytoolslib.models import detect_version, get_device_info
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_ips():
|
|
15
|
+
"""Return list of IPs to check."""
|
|
16
|
+
try:
|
|
17
|
+
gateways = netifaces.gateways()
|
|
18
|
+
interfaces = []
|
|
19
|
+
for key, value in gateways.items():
|
|
20
|
+
if key != "default":
|
|
21
|
+
for item in value:
|
|
22
|
+
interfaces.append(item[1])
|
|
23
|
+
addresses = set()
|
|
24
|
+
for interface in interfaces:
|
|
25
|
+
addresses_tmp = netifaces.ifaddresses(interface).get(2)
|
|
26
|
+
if addresses_tmp:
|
|
27
|
+
for addr in addresses_tmp:
|
|
28
|
+
addresses.add(addr["addr"])
|
|
29
|
+
except ValueError:
|
|
30
|
+
addresses = socket.gethostbyname_ex(socket.gethostname())[2]
|
|
31
|
+
return addresses
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_discovery_single(address, timelimit=3):
|
|
35
|
+
"""Run discovery for given address (tuple) in timelimit."""
|
|
36
|
+
devices = []
|
|
37
|
+
with DiscoveryServer(address, DiscoveryHandler) as server:
|
|
38
|
+
server_thread = threading.Thread(target=server.serve_forever)
|
|
39
|
+
server_thread.start()
|
|
40
|
+
time.sleep(timelimit)
|
|
41
|
+
server.shutdown()
|
|
42
|
+
devices = server.devices.copy()
|
|
43
|
+
del server_thread
|
|
44
|
+
return devices
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def serve_forever(server):
|
|
48
|
+
"""Wrapper for serve_forever method of server."""
|
|
49
|
+
server.serve_forever()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_discovery_all(timelimit=3, port=LK_UDP_PORT, version=2, addresses=None):
|
|
53
|
+
"""Run discovery on all available addresses.
|
|
54
|
+
|
|
55
|
+
`version` - 1 and 2 are parallel, where time execution of 2
|
|
56
|
+
is closer to timelimit; 3 is sequential run;
|
|
57
|
+
"""
|
|
58
|
+
if addresses is None:
|
|
59
|
+
addresses = [ip for ip in get_ips() if not ip.startswith("169.254")]
|
|
60
|
+
devices = []
|
|
61
|
+
if version == 1:
|
|
62
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
|
63
|
+
servers = [
|
|
64
|
+
DiscoveryServer((address, port), DiscoveryHandler)
|
|
65
|
+
for address in addresses
|
|
66
|
+
]
|
|
67
|
+
executor.map(serve_forever, servers)
|
|
68
|
+
time.sleep(timelimit)
|
|
69
|
+
for server in servers:
|
|
70
|
+
server.shutdown()
|
|
71
|
+
devices.extend(server.devices)
|
|
72
|
+
elif version == 2:
|
|
73
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
|
74
|
+
servers = [
|
|
75
|
+
DiscoveryServerAuto(
|
|
76
|
+
(address, port), DiscoveryHandler, timelimit=timelimit
|
|
77
|
+
)
|
|
78
|
+
for address in addresses
|
|
79
|
+
]
|
|
80
|
+
futures = {
|
|
81
|
+
executor.submit(server.serve_forever): server for server in servers
|
|
82
|
+
}
|
|
83
|
+
for future in concurrent.futures.as_completed(futures):
|
|
84
|
+
try:
|
|
85
|
+
data = future.result()
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
logging.warning("discovery error: %s", str(exc))
|
|
88
|
+
else:
|
|
89
|
+
devices.extend(data)
|
|
90
|
+
else:
|
|
91
|
+
for address in addresses:
|
|
92
|
+
temp = run_discovery_single((address, port))
|
|
93
|
+
devices.extend(temp)
|
|
94
|
+
return devices
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DiscoveryHandler(socketserver.BaseRequestHandler):
|
|
98
|
+
"""Handler for LK discovery server."""
|
|
99
|
+
|
|
100
|
+
def handle(self):
|
|
101
|
+
data = self.request[0].strip()
|
|
102
|
+
if self.server.server_address[0] != self.client_address[0]:
|
|
103
|
+
try:
|
|
104
|
+
device_response = data.decode(errors="ignore").splitlines()
|
|
105
|
+
device_data = {
|
|
106
|
+
"ip_address": self.client_address[0],
|
|
107
|
+
"name": device_response[0],
|
|
108
|
+
"mac_address": device_response[1].replace("-", ":"),
|
|
109
|
+
"software_version": None,
|
|
110
|
+
"hardware_version": None,
|
|
111
|
+
}
|
|
112
|
+
if len(device_response) == 4:
|
|
113
|
+
device_data["software_version"] = device_response[2]
|
|
114
|
+
device_data["hardware_version"] = device_response[3]
|
|
115
|
+
else:
|
|
116
|
+
# LK2.0/2.5 response do not include hardware_version, so detect it.
|
|
117
|
+
device_data["software_version"] = device_response[2][:-1]
|
|
118
|
+
device_data["hardware_version"] = detect_version(
|
|
119
|
+
device_data["software_version"]
|
|
120
|
+
)
|
|
121
|
+
device_data.update(
|
|
122
|
+
get_device_info(
|
|
123
|
+
device_data["hardware_version"],
|
|
124
|
+
device_data["software_version"],
|
|
125
|
+
asdict_=True,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
self.server.devices.append(device_data)
|
|
129
|
+
except (UnicodeDecodeError, IndexError):
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class DiscoveryServer(socketserver.UDPServer):
|
|
134
|
+
"""Server for finding LKs in networks."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, *args, **kwargs):
|
|
137
|
+
super().__init__(*args, **kwargs)
|
|
138
|
+
self.devices = []
|
|
139
|
+
|
|
140
|
+
def server_activate(self):
|
|
141
|
+
"""Send discovery message after activation."""
|
|
142
|
+
super().server_activate()
|
|
143
|
+
dst_ip = ".".join(self.server_address[0].split(".")[:3]) + ".255"
|
|
144
|
+
dst_port = self.server_address[1]
|
|
145
|
+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
146
|
+
self.socket.sendto(LK_UDP_DISCOVERY_MSG, (dst_ip, dst_port))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class DiscoveryServerAuto(DiscoveryServer):
|
|
150
|
+
"""DiscoveryServer that autmatically shuts down."""
|
|
151
|
+
|
|
152
|
+
def __init__(self, *args, **kwargs):
|
|
153
|
+
self.started_at = time.time()
|
|
154
|
+
self.timelimit = kwargs.pop("timelimit", 3)
|
|
155
|
+
super().__init__(*args, **kwargs)
|
|
156
|
+
|
|
157
|
+
def service_actions(self):
|
|
158
|
+
super().service_actions()
|
|
159
|
+
now = time.time()
|
|
160
|
+
if now - self.started_at >= self.timelimit:
|
|
161
|
+
if not self._BaseServer__shutdown_request:
|
|
162
|
+
self._BaseServer__shutdown_request = True
|
|
163
|
+
|
|
164
|
+
def serve_forever(self, *args, **kwargs):
|
|
165
|
+
"""Return devices list after auto shutdown."""
|
|
166
|
+
super().serve_forever(*args, **kwargs)
|
|
167
|
+
return self.devices
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Exceptions for tinyToolsLib."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TinyToolsError(Exception):
|
|
5
|
+
"""Generic tinyTools exception."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TinyToolsUnsupported(TinyToolsError):
|
|
9
|
+
"""Unsupported action of tinycontrol device."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# region Request related errors
|
|
13
|
+
class TinyToolsRequestError(TinyToolsError):
|
|
14
|
+
"""Base exception for request errors."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TinyToolsRequestTimeout(TinyToolsRequestError):
|
|
18
|
+
"""Request timed out while trying to connect to server."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TinyToolsRequestConnectionError(TinyToolsRequestError):
|
|
22
|
+
"""Connection error occurred."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TinyToolsRequestSSLError(TinyToolsRequestConnectionError):
|
|
26
|
+
"""An SSL error occurred."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TinyToolsRequestHTTPError(TinyToolsRequestError):
|
|
30
|
+
"""HTTP error occurred while handling request(HTTP 4xx/5xx)."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TinyToolsRequestUnauthenticated(TinyToolsRequestHTTPError):
|
|
34
|
+
"""Request requires authentication or is invalid (HTTP 401)."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TinyToolsRequestNotFound(TinyToolsRequestHTTPError):
|
|
38
|
+
"""Server could not find the requested resource (HTTP 404)."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TinyToolsRequestInternalServerError(TinyToolsRequestHTTPError):
|
|
42
|
+
"""Server has encountered a situation it does not know how to handle. (HTTP 500)."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# endregion
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TinyToolsFlashError(TinyToolsError):
|
|
49
|
+
"""tinyTools flashing exception."""
|
tinytoolslib/flash.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import socket
|
|
4
|
+
import time
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from math import ceil
|
|
7
|
+
from threading import Event
|
|
8
|
+
|
|
9
|
+
from tftpy import SOCK_TIMEOUT, TftpClient, TftpTimeout
|
|
10
|
+
|
|
11
|
+
from tinytoolslib.constants import LK_UDP_BOOTLOADER_MSG, LK_UDP_PORT
|
|
12
|
+
from tinytoolslib.requests import get, post
|
|
13
|
+
from tinytoolslib.exceptions import TinyToolsFlashError, TinyToolsRequestError
|
|
14
|
+
from tinytoolslib.models import (
|
|
15
|
+
LK_HW_20,
|
|
16
|
+
LK_HW_20_PS,
|
|
17
|
+
LK_HW_25,
|
|
18
|
+
LK_HW_25_PS,
|
|
19
|
+
get_version,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Flasher:
|
|
24
|
+
"""Class with all flash related functions.
|
|
25
|
+
|
|
26
|
+
Generally use as:
|
|
27
|
+
flasher = Flasher()
|
|
28
|
+
flasher.run(...)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, callback=None):
|
|
32
|
+
"""Initialize Flasher.
|
|
33
|
+
|
|
34
|
+
callback - None/function that will be called during flashing
|
|
35
|
+
with progress information. 4 parameters: current, total,
|
|
36
|
+
percent, unit ('packet', 'B').
|
|
37
|
+
"""
|
|
38
|
+
self.callback = callback
|
|
39
|
+
self.context = {}
|
|
40
|
+
self.canceled = Event()
|
|
41
|
+
|
|
42
|
+
# region TFTP flashing
|
|
43
|
+
@staticmethod
|
|
44
|
+
def get_optimal_number_of_attempts(version_info):
|
|
45
|
+
"""Return number of attempts, so for HW2.X it quits earlier.
|
|
46
|
+
|
|
47
|
+
Each attempt takes SOCK_TIMEOUT*TIMEOUT_RETRIES
|
|
48
|
+
"""
|
|
49
|
+
logging.debug("Getting optimal number of flash attempts (less for LK2.X)")
|
|
50
|
+
lk_2X_models = [
|
|
51
|
+
LK_HW_20_PS.info.model,
|
|
52
|
+
LK_HW_20.info.model,
|
|
53
|
+
LK_HW_25.info.model,
|
|
54
|
+
LK_HW_25_PS.info.model,
|
|
55
|
+
]
|
|
56
|
+
if version_info is not None and version_info["model"] in lk_2X_models:
|
|
57
|
+
return 1
|
|
58
|
+
return 4
|
|
59
|
+
|
|
60
|
+
def start_bootloader(self, host, username, password, schema, port):
|
|
61
|
+
"""Start bootloader mode on device (LK2.X, LK3.X)."""
|
|
62
|
+
success = False
|
|
63
|
+
# First try http method.
|
|
64
|
+
try:
|
|
65
|
+
# First check if upgrade is enabled else enable it
|
|
66
|
+
resp = get(host, "/xml/st.xml", schema, port, username, password)["parsed"]
|
|
67
|
+
if resp.get("upgr") == "0":
|
|
68
|
+
logging.info("Upgrade is disabled on device - trying to enable it.")
|
|
69
|
+
cmd = "/stm.cgi?auth={}{}{}".format(resp["auth"], 1, resp["userpass"])
|
|
70
|
+
get(host, cmd, schema, port, username, password)["parsed"]
|
|
71
|
+
logging.info("Starting bootloader mode via HTTP...")
|
|
72
|
+
get(host, "/stm.cgi?upgrade=lkstart3", schema, port, username, password)
|
|
73
|
+
success = True
|
|
74
|
+
except (KeyError, ValueError, TinyToolsRequestError) as exc:
|
|
75
|
+
logging.error("Failed to enable bootloader via HTTP: %s", str(exc))
|
|
76
|
+
if not success:
|
|
77
|
+
# Try UDP method.
|
|
78
|
+
logging.info("Starting bootloader mode via UDP...")
|
|
79
|
+
with socket.socket(
|
|
80
|
+
type=socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP
|
|
81
|
+
) as sock:
|
|
82
|
+
sock.connect((host, LK_UDP_PORT))
|
|
83
|
+
sock.sendall(LK_UDP_BOOTLOADER_MSG)
|
|
84
|
+
success = True
|
|
85
|
+
return success
|
|
86
|
+
|
|
87
|
+
def flash_hook(self, packet):
|
|
88
|
+
"""Display flashing progress."""
|
|
89
|
+
if self.canceled.is_set() and packet.opcode == 2:
|
|
90
|
+
# Cancel if stop flag set and still waiting for transfer.
|
|
91
|
+
raise TinyToolsFlashError("Flash canceled by user")
|
|
92
|
+
if packet.opcode == 3:
|
|
93
|
+
logging.debug("Packet %d/%d", packet.blocknumber, self.context["packets"])
|
|
94
|
+
if callable(self.callback):
|
|
95
|
+
# Call with <packet no>, <total packets>, <progress %>
|
|
96
|
+
self.callback(
|
|
97
|
+
packet.blocknumber,
|
|
98
|
+
self.context["packets"],
|
|
99
|
+
packet.blocknumber / self.context["packets"] * 100,
|
|
100
|
+
"packet",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def flash_firmware_via_tftp(self, host, firmware_path, attempts_limit):
|
|
104
|
+
"""Try to flash firmware and display progress."""
|
|
105
|
+
firmware_name = os.path.basename(firmware_path)
|
|
106
|
+
bytes_size = os.stat(firmware_path).st_size
|
|
107
|
+
self.context.update(
|
|
108
|
+
{
|
|
109
|
+
"size": bytes_size,
|
|
110
|
+
"packets": ceil(bytes_size / 512),
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
logging.info(
|
|
114
|
+
"Uploading firmware %s with size of %dB in %d packets",
|
|
115
|
+
firmware_name,
|
|
116
|
+
self.context["size"],
|
|
117
|
+
self.context["packets"],
|
|
118
|
+
)
|
|
119
|
+
client = TftpClient(host)
|
|
120
|
+
attempt = 0
|
|
121
|
+
flashed = False
|
|
122
|
+
canceled = False
|
|
123
|
+
while attempt < attempts_limit and not flashed:
|
|
124
|
+
try:
|
|
125
|
+
if self.canceled.is_set():
|
|
126
|
+
# Stop before starting flash.
|
|
127
|
+
raise TinyToolsFlashError("Flash canceled by user")
|
|
128
|
+
client.upload(firmware_name, firmware_path, self.flash_hook)
|
|
129
|
+
except TftpTimeout:
|
|
130
|
+
attempt += 1
|
|
131
|
+
except (ConnectionError, socket.gaierror):
|
|
132
|
+
attempt += 1
|
|
133
|
+
time.sleep(SOCK_TIMEOUT)
|
|
134
|
+
except TinyToolsFlashError:
|
|
135
|
+
canceled = True
|
|
136
|
+
break
|
|
137
|
+
else:
|
|
138
|
+
flashed = True
|
|
139
|
+
if canceled:
|
|
140
|
+
logging.info("Canceled flashing")
|
|
141
|
+
return False
|
|
142
|
+
elif not flashed:
|
|
143
|
+
logging.warning("Unable to connect with device. Try again.")
|
|
144
|
+
return False
|
|
145
|
+
else:
|
|
146
|
+
logging.info(
|
|
147
|
+
"Uploaded firmware in %.1fs with avg speed of %.0f kbps.",
|
|
148
|
+
client.context.metrics.duration,
|
|
149
|
+
client.context.metrics.kbps,
|
|
150
|
+
)
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
# endregion
|
|
154
|
+
|
|
155
|
+
def update_firmware_via_http(
|
|
156
|
+
self, firmware_path, host, username, password, schema, port
|
|
157
|
+
):
|
|
158
|
+
"""Update firmware via HTTP for LK4/tcPDU."""
|
|
159
|
+
try:
|
|
160
|
+
with open(firmware_path, "rb") as fread:
|
|
161
|
+
bytes_size = os.stat(firmware_path).st_size
|
|
162
|
+
self.context.update(
|
|
163
|
+
{
|
|
164
|
+
"size": bytes_size,
|
|
165
|
+
"uploaded": 0,
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
# Modify stream object to update progress
|
|
169
|
+
func = getattr(fread, "read")
|
|
170
|
+
|
|
171
|
+
@wraps(func)
|
|
172
|
+
def read(data, *args, **kwargs):
|
|
173
|
+
res = func(data, *args, **kwargs)
|
|
174
|
+
self.context["uploaded"] += data
|
|
175
|
+
if self.context["uploaded"] > self.context["size"]:
|
|
176
|
+
self.context["uploaded"] = self.context["size"]
|
|
177
|
+
logging.debug(
|
|
178
|
+
"Uploaded %.0f/%.0f kB (%.1f %%)",
|
|
179
|
+
self.context["uploaded"] / 1024,
|
|
180
|
+
self.context["size"] / 1024,
|
|
181
|
+
self.context["uploaded"] / self.context["size"] * 100,
|
|
182
|
+
)
|
|
183
|
+
if callable(self.callback):
|
|
184
|
+
# Call with <uploaded B>, <total B>, <progress %>
|
|
185
|
+
self.callback(
|
|
186
|
+
self.context["uploaded"],
|
|
187
|
+
self.context["size"],
|
|
188
|
+
self.context["uploaded"] / self.context["size"] * 100,
|
|
189
|
+
"B",
|
|
190
|
+
)
|
|
191
|
+
return res
|
|
192
|
+
|
|
193
|
+
setattr(fread, "read", read)
|
|
194
|
+
# Upload file
|
|
195
|
+
if callable(self.callback):
|
|
196
|
+
# Call with <0 B>, <total B>, <0 %>
|
|
197
|
+
self.callback(0, self.context["size"], 0, "B")
|
|
198
|
+
resp = post(
|
|
199
|
+
host,
|
|
200
|
+
"/api/v1/upload_firmware/new_firmware",
|
|
201
|
+
fread,
|
|
202
|
+
schema,
|
|
203
|
+
port,
|
|
204
|
+
username,
|
|
205
|
+
password,
|
|
206
|
+
)
|
|
207
|
+
# Restart device
|
|
208
|
+
get(host, "/api/v1/save/?restart=1", schema, port, username, password)
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
logging.warning("Error occurred: %s. Try again.", str(exc))
|
|
211
|
+
return False
|
|
212
|
+
else:
|
|
213
|
+
logging.info(
|
|
214
|
+
"Uploaded firmware in %.1fs with avg speed of %.0f kbps.",
|
|
215
|
+
resp.elapsed.total_seconds(),
|
|
216
|
+
self.context["size"] / 1024 * 8 / resp.elapsed.total_seconds(),
|
|
217
|
+
)
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
def run(
|
|
221
|
+
self, firmware_path, host, username, password, port=80, progress_callback=None
|
|
222
|
+
):
|
|
223
|
+
"""Main entry to flashing.
|
|
224
|
+
|
|
225
|
+
Depending on input and data found from device it will select
|
|
226
|
+
tftp or http method.
|
|
227
|
+
"""
|
|
228
|
+
if progress_callback is not None:
|
|
229
|
+
self.callback = progress_callback
|
|
230
|
+
self.context = {}
|
|
231
|
+
if (
|
|
232
|
+
isinstance(firmware_path, str)
|
|
233
|
+
and firmware_path
|
|
234
|
+
and os.path.isfile(firmware_path)
|
|
235
|
+
):
|
|
236
|
+
# Try to check what device type are we working with. For now,
|
|
237
|
+
# assume that lk4/tcpdu always respond via HTTP.
|
|
238
|
+
version_info = get_version(host, port, username, password)
|
|
239
|
+
if version_info and version_info["network_info"].get("schema") == "https":
|
|
240
|
+
schema = "https"
|
|
241
|
+
port = 443
|
|
242
|
+
else:
|
|
243
|
+
schema = "http"
|
|
244
|
+
if version_info and version_info.get("http_update"):
|
|
245
|
+
return self.update_firmware_via_http(
|
|
246
|
+
firmware_path, host, username, password, schema, port
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
attempts = self.get_optimal_number_of_attempts(version_info)
|
|
250
|
+
logging.info("Preparing device for flashing...")
|
|
251
|
+
self.start_bootloader(host, username, password, schema, port)
|
|
252
|
+
return self.flash_firmware_via_tftp(host, firmware_path, attempts)
|
|
253
|
+
else:
|
|
254
|
+
logging.warning("Invalid file for flashing.")
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# region getting new firmware file
|
|
259
|
+
def check_for_latest_firmware(version_info):
|
|
260
|
+
"""Check latest available version of firmware."""
|
|
261
|
+
if version_info["fw_url"] is None:
|
|
262
|
+
return (
|
|
263
|
+
False,
|
|
264
|
+
"Cannot get firmware files for this device directly. "
|
|
265
|
+
"You can look for it at https://tinycontrol.pl.",
|
|
266
|
+
)
|
|
267
|
+
try:
|
|
268
|
+
resp = get(version_info["fw_url"], None, timeout=5)
|
|
269
|
+
except TinyToolsRequestError as exc:
|
|
270
|
+
return False, str(exc)
|
|
271
|
+
else:
|
|
272
|
+
result = resp["parsed"]
|
|
273
|
+
result.update({"hardware_version": version_info["hardware_version"]})
|
|
274
|
+
return True, result
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_latest_firmware(host, username, password, firmware_directory):
|
|
278
|
+
"""Get latest firmware for device."""
|
|
279
|
+
version_info = get_version(host, username=username, password=password)
|
|
280
|
+
if version_info:
|
|
281
|
+
latest_version = check_for_latest_firmware(version_info)
|
|
282
|
+
if latest_version[0]:
|
|
283
|
+
# Check if downloaded else download
|
|
284
|
+
firmware_name = latest_version[1]["url"].split("/")[-1]
|
|
285
|
+
firmware_path = os.path.join(firmware_directory, firmware_name)
|
|
286
|
+
if os.path.isfile(firmware_path) or download_firmware(
|
|
287
|
+
latest_version[1]["url"], firmware_path
|
|
288
|
+
):
|
|
289
|
+
version_info.update(
|
|
290
|
+
{
|
|
291
|
+
"path": firmware_path,
|
|
292
|
+
"new_sw": latest_version[1]["name"],
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
return True, version_info
|
|
296
|
+
else:
|
|
297
|
+
return False, "Failed to download file"
|
|
298
|
+
else:
|
|
299
|
+
return False, latest_version[1]
|
|
300
|
+
return False, "Cannot get information about latest firmware"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def download_firmware(download_url, save_location):
|
|
304
|
+
"""Download firmware from given url."""
|
|
305
|
+
try:
|
|
306
|
+
resp = get(download_url, None, timeout=5)
|
|
307
|
+
except TinyToolsRequestError:
|
|
308
|
+
return False
|
|
309
|
+
else:
|
|
310
|
+
os.makedirs(os.path.dirname(save_location), exist_ok=True)
|
|
311
|
+
with open(save_location, "wb") as f:
|
|
312
|
+
f.write(resp["_response"].content)
|
|
313
|
+
return True
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# endregion
|