hubble-gateway-sdk-python 0.1.0__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.
- hubble_gateway/__init__.py +47 -0
- hubble_gateway/auth.py +255 -0
- hubble_gateway/config.py +109 -0
- hubble_gateway/location.py +423 -0
- hubble_gateway/models.py +118 -0
- hubble_gateway/scanner.py +154 -0
- hubble_gateway/sender.py +202 -0
- hubble_gateway/ubx.py +230 -0
- hubble_gateway_sdk_python-0.1.0.dist-info/METADATA +44 -0
- hubble_gateway_sdk_python-0.1.0.dist-info/RECORD +12 -0
- hubble_gateway_sdk_python-0.1.0.dist-info/WHEEL +4 -0
- hubble_gateway_sdk_python-0.1.0.dist-info/licenses/LICENSE +203 -0
hubble_gateway/sender.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Packet sender: batches, deduplicates, and uploads BLE sightings."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
import uuid as uuid_mod
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
from hubble_gateway.auth import GatewayAuth
|
|
12
|
+
from hubble_gateway.models import BLEPacket, Location
|
|
13
|
+
|
|
14
|
+
logger = structlog.get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _DedupEntry:
|
|
18
|
+
__slots__ = ("packet", "strongest_rssi", "sighting_count", "first_seen_ms")
|
|
19
|
+
|
|
20
|
+
def __init__(self, packet: BLEPacket) -> None:
|
|
21
|
+
self.packet = packet
|
|
22
|
+
self.strongest_rssi = packet.rssi
|
|
23
|
+
self.sighting_count = 1
|
|
24
|
+
self.first_seen_ms = int(packet.timestamp.timestamp() * 1000)
|
|
25
|
+
|
|
26
|
+
def merge(self, packet: BLEPacket) -> None:
|
|
27
|
+
self.sighting_count += 1
|
|
28
|
+
if packet.rssi > self.strongest_rssi:
|
|
29
|
+
self.strongest_rssi = packet.rssi
|
|
30
|
+
self.packet = packet
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GatewaySender:
|
|
34
|
+
"""Batches and uploads BLE sightings to the gateway API."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
auth: GatewayAuth,
|
|
39
|
+
get_location: Callable[[], Awaitable[Location]] | None = None,
|
|
40
|
+
batch_size: int = 500,
|
|
41
|
+
upload_interval_s: float = 5.0,
|
|
42
|
+
dedup_window_s: float = 300.0,
|
|
43
|
+
) -> None:
|
|
44
|
+
self._auth = auth
|
|
45
|
+
self._get_location = get_location
|
|
46
|
+
self._batch_size = batch_size
|
|
47
|
+
self._upload_interval = upload_interval_s
|
|
48
|
+
self._dedup_window = dedup_window_s
|
|
49
|
+
self._dedup_map: dict[tuple[str, str | None, str], _DedupEntry] = {}
|
|
50
|
+
self._dedup_lock = asyncio.Lock()
|
|
51
|
+
self._running = False
|
|
52
|
+
self._stats_sends = 0
|
|
53
|
+
self._stats_total = 0
|
|
54
|
+
self._stats_uploaded = 0
|
|
55
|
+
self._stats_devices: set[str] = set()
|
|
56
|
+
|
|
57
|
+
async def start(self) -> None:
|
|
58
|
+
self._running = True
|
|
59
|
+
self._apply_server_config()
|
|
60
|
+
asyncio.create_task(self._upload_loop())
|
|
61
|
+
if self._get_location:
|
|
62
|
+
asyncio.create_task(self._location_loop())
|
|
63
|
+
logger.info(
|
|
64
|
+
"Sender started",
|
|
65
|
+
gateway_id=self._auth.gateway_id,
|
|
66
|
+
upload_interval=self._upload_interval,
|
|
67
|
+
dedup_window=self._dedup_window,
|
|
68
|
+
batch_size=self._batch_size,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async def stop(self) -> None:
|
|
72
|
+
self._running = False
|
|
73
|
+
await self._flush()
|
|
74
|
+
logger.info(
|
|
75
|
+
"Sender stopped",
|
|
76
|
+
total_packets=self._stats_total,
|
|
77
|
+
uploaded=self._stats_uploaded,
|
|
78
|
+
sends=self._stats_sends,
|
|
79
|
+
unique_devices=len(self._stats_devices),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def add_packet(self, packet: BLEPacket) -> None:
|
|
83
|
+
self._stats_total += 1
|
|
84
|
+
self._stats_devices.add(packet.device_address)
|
|
85
|
+
key = (packet.device_address, packet.service_uuid_16bit, packet.service_data_hex)
|
|
86
|
+
existing = self._dedup_map.get(key)
|
|
87
|
+
if existing is not None:
|
|
88
|
+
age_ms = int(packet.timestamp.timestamp() * 1000) - existing.first_seen_ms
|
|
89
|
+
if age_ms < self._dedup_window * 1000:
|
|
90
|
+
existing.merge(packet)
|
|
91
|
+
return
|
|
92
|
+
self._dedup_map[key] = _DedupEntry(packet)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def stats(self) -> dict[str, int]:
|
|
96
|
+
return {
|
|
97
|
+
"total_packets": self._stats_total,
|
|
98
|
+
"uploaded_packets": self._stats_uploaded,
|
|
99
|
+
"sends": self._stats_sends,
|
|
100
|
+
"unique_devices": len(self._stats_devices),
|
|
101
|
+
"buffer_size": len(self._dedup_map),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async def _upload_loop(self) -> None:
|
|
105
|
+
while self._running:
|
|
106
|
+
await asyncio.sleep(self._upload_interval)
|
|
107
|
+
if not self._dedup_map:
|
|
108
|
+
continue
|
|
109
|
+
await self._flush()
|
|
110
|
+
|
|
111
|
+
async def _location_loop(self) -> None:
|
|
112
|
+
_INTERVAL = 60.0
|
|
113
|
+
while self._running:
|
|
114
|
+
await asyncio.sleep(_INTERVAL)
|
|
115
|
+
if not self._get_location:
|
|
116
|
+
continue
|
|
117
|
+
try:
|
|
118
|
+
location = await self._get_location()
|
|
119
|
+
if location.latitude is not None and location.longitude is not None:
|
|
120
|
+
await self._upload_location(location)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.warning("Location upload error", error=str(e))
|
|
123
|
+
|
|
124
|
+
async def _flush(self) -> None:
|
|
125
|
+
async with self._dedup_lock:
|
|
126
|
+
if not self._dedup_map:
|
|
127
|
+
return
|
|
128
|
+
entries = list(self._dedup_map.values())
|
|
129
|
+
self._dedup_map.clear()
|
|
130
|
+
|
|
131
|
+
location = Location()
|
|
132
|
+
if self._get_location:
|
|
133
|
+
try:
|
|
134
|
+
location = await self._get_location()
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.warning("Failed to get location", error=str(e))
|
|
137
|
+
|
|
138
|
+
for i in range(0, len(entries), self._batch_size):
|
|
139
|
+
batch = entries[i : i + self._batch_size]
|
|
140
|
+
await self._upload_batch(batch, location)
|
|
141
|
+
|
|
142
|
+
async def _upload_batch(self, entries: list[_DedupEntry], location: Location) -> None:
|
|
143
|
+
gateway_id = self._auth.gateway_id
|
|
144
|
+
if not gateway_id:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
batch_id = str(uuid_mod.uuid4())
|
|
148
|
+
packets_json = []
|
|
149
|
+
for entry in entries:
|
|
150
|
+
pkt = entry.packet.to_gateway_packet(
|
|
151
|
+
location=location, sighting_count=entry.sighting_count,
|
|
152
|
+
)
|
|
153
|
+
pkt["rssi"] = entry.strongest_rssi
|
|
154
|
+
packets_json.append(pkt)
|
|
155
|
+
|
|
156
|
+
body: dict[str, Any] = {
|
|
157
|
+
"batchId": batch_id,
|
|
158
|
+
"sentAtMillis": int(time.time() * 1000),
|
|
159
|
+
"packets": packets_json,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
resp = await self._auth.authenticated_request(
|
|
164
|
+
"POST", f"/api/v1/gateways/{gateway_id}/packets", json_body=body,
|
|
165
|
+
)
|
|
166
|
+
if resp.status_code in (200, 201, 202):
|
|
167
|
+
self._stats_sends += 1
|
|
168
|
+
self._stats_uploaded += len(entries)
|
|
169
|
+
sightings = sum(e.sighting_count for e in entries)
|
|
170
|
+
logger.info(
|
|
171
|
+
"Batch uploaded",
|
|
172
|
+
batch_id=batch_id[:8],
|
|
173
|
+
packets=len(entries),
|
|
174
|
+
sightings=sightings,
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
logger.warning("Upload failed", status=resp.status_code, body=resp.text[:200])
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error("Upload error", error=str(e))
|
|
180
|
+
|
|
181
|
+
async def _upload_location(self, location: Location) -> None:
|
|
182
|
+
gateway_id = self._auth.gateway_id
|
|
183
|
+
if not gateway_id:
|
|
184
|
+
return
|
|
185
|
+
body: dict[str, Any] = {
|
|
186
|
+
"batchId": str(uuid_mod.uuid4()),
|
|
187
|
+
"sentAtMillis": int(time.time() * 1000),
|
|
188
|
+
"locations": [location.to_gateway_dict()],
|
|
189
|
+
}
|
|
190
|
+
try:
|
|
191
|
+
await self._auth.authenticated_request(
|
|
192
|
+
"POST", f"/api/v1/gateways/{gateway_id}/locations", json_body=body,
|
|
193
|
+
)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.warning("Location upload failed", error=str(e))
|
|
196
|
+
|
|
197
|
+
def _apply_server_config(self) -> None:
|
|
198
|
+
cfg = self._auth.server_config
|
|
199
|
+
if "uploadBatchSize" in cfg:
|
|
200
|
+
self._batch_size = min(int(cfg["uploadBatchSize"]), 500)
|
|
201
|
+
if "uploadIntervalMs" in cfg:
|
|
202
|
+
self._upload_interval = max(int(cfg["uploadIntervalMs"]) / 1000, 1)
|
hubble_gateway/ubx.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Async UBX NAV-PVT reader for u-blox receivers (ZED-F9P, NEO-M9N, etc.)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import struct
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
logger = structlog.get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
UBX_SYNC_1 = 0xB5
|
|
14
|
+
UBX_SYNC_2 = 0x62
|
|
15
|
+
NAV_CLASS = 0x01
|
|
16
|
+
NAV_PVT_ID = 0x07
|
|
17
|
+
NAV_PVT_PAYLOAD_LEN = 92
|
|
18
|
+
READ_PVT_TIMEOUT_S = 10.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ubx_checksum(data: bytes) -> tuple[int, int]:
|
|
22
|
+
ck_a = 0
|
|
23
|
+
ck_b = 0
|
|
24
|
+
for byte in data:
|
|
25
|
+
ck_a = (ck_a + byte) & 0xFF
|
|
26
|
+
ck_b = (ck_b + ck_a) & 0xFF
|
|
27
|
+
return ck_a, ck_b
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ubx_frame(msg_class: int, msg_id: int, payload: bytes) -> bytes:
|
|
31
|
+
header = struct.pack("<BBH", msg_class, msg_id, len(payload))
|
|
32
|
+
body = header + payload
|
|
33
|
+
ck_a, ck_b = _ubx_checksum(body)
|
|
34
|
+
return b"\xb5\x62" + body + bytes([ck_a, ck_b])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
NAV_PVT_POLL = _ubx_frame(NAV_CLASS, NAV_PVT_ID, b"")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(slots=True)
|
|
41
|
+
class UBXNavPVT:
|
|
42
|
+
itow_ms: int
|
|
43
|
+
year: int
|
|
44
|
+
month: int
|
|
45
|
+
day: int
|
|
46
|
+
hour: int
|
|
47
|
+
minute: int
|
|
48
|
+
second: int
|
|
49
|
+
valid: int
|
|
50
|
+
valid_date: bool
|
|
51
|
+
valid_time: bool
|
|
52
|
+
fix_type: int
|
|
53
|
+
flags: int
|
|
54
|
+
gnss_fix_ok: bool
|
|
55
|
+
num_sv: int
|
|
56
|
+
lat: float
|
|
57
|
+
lon: float
|
|
58
|
+
height_ellipsoid_m: float
|
|
59
|
+
alt_msl_m: float
|
|
60
|
+
hacc_m: float
|
|
61
|
+
vacc_m: float
|
|
62
|
+
ground_speed_ms: float
|
|
63
|
+
heading_deg: float
|
|
64
|
+
pdop: float
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse_nav_pvt_payload(payload: bytes) -> UBXNavPVT:
|
|
68
|
+
if len(payload) != NAV_PVT_PAYLOAD_LEN:
|
|
69
|
+
raise ValueError(f"NAV-PVT payload length {len(payload)}, expected {NAV_PVT_PAYLOAD_LEN}")
|
|
70
|
+
|
|
71
|
+
itow_ms = struct.unpack_from("<I", payload, 0)[0]
|
|
72
|
+
year = struct.unpack_from("<H", payload, 4)[0]
|
|
73
|
+
month, day, hour, minute, second, valid = struct.unpack_from("<BBBBBB", payload, 6)
|
|
74
|
+
fix_type = payload[20]
|
|
75
|
+
flags = payload[21]
|
|
76
|
+
num_sv = payload[23]
|
|
77
|
+
lon_i, lat_i, height_mm, hmsl_mm = struct.unpack_from("<iiii", payload, 24)
|
|
78
|
+
hacc_mm, vacc_mm = struct.unpack_from("<II", payload, 40)
|
|
79
|
+
g_speed_mm_s, head_mot = struct.unpack_from("<ii", payload, 60)
|
|
80
|
+
pdop_raw = struct.unpack_from("<H", payload, 76)[0]
|
|
81
|
+
|
|
82
|
+
return UBXNavPVT(
|
|
83
|
+
itow_ms=itow_ms, year=year, month=month, day=day,
|
|
84
|
+
hour=hour, minute=minute, second=second, valid=valid,
|
|
85
|
+
valid_date=bool(valid & 0x01), valid_time=bool(valid & 0x02),
|
|
86
|
+
fix_type=fix_type, flags=flags, gnss_fix_ok=bool(flags & 0x01),
|
|
87
|
+
num_sv=num_sv,
|
|
88
|
+
lat=lat_i / 1e7, lon=lon_i / 1e7,
|
|
89
|
+
height_ellipsoid_m=height_mm / 1000.0, alt_msl_m=hmsl_mm / 1000.0,
|
|
90
|
+
hacc_m=hacc_mm / 1000.0, vacc_m=vacc_mm / 1000.0,
|
|
91
|
+
ground_speed_ms=g_speed_mm_s / 1000.0, heading_deg=head_mot / 1e5,
|
|
92
|
+
pdop=pdop_raw / 100.0,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class UBXReader:
|
|
97
|
+
"""Async UBX reader that polls NAV-PVT from a serial port."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, port: str, baud_rate: int = 38400) -> None:
|
|
100
|
+
self._port = port
|
|
101
|
+
self._baud_rate = baud_rate
|
|
102
|
+
self._reader: asyncio.StreamReader | None = None
|
|
103
|
+
self._writer: asyncio.StreamWriter | None = None
|
|
104
|
+
self._buf = bytearray()
|
|
105
|
+
|
|
106
|
+
async def start(self) -> None:
|
|
107
|
+
import serial_asyncio
|
|
108
|
+
|
|
109
|
+
self._reader, self._writer = await serial_asyncio.open_serial_connection(
|
|
110
|
+
url=self._port, baudrate=self._baud_rate,
|
|
111
|
+
)
|
|
112
|
+
self._buf.clear()
|
|
113
|
+
logger.info("UBX reader connected", port=self._port, baud_rate=self._baud_rate)
|
|
114
|
+
await self._configure_uart()
|
|
115
|
+
|
|
116
|
+
async def _configure_uart(self) -> None:
|
|
117
|
+
"""Enable UBX NAV-PVT output on UART (persisted to flash)."""
|
|
118
|
+
if self._writer is None:
|
|
119
|
+
return
|
|
120
|
+
kv = struct.pack("<IB", 0x10750001, 1) # CFG-UART2OUTPROT-UBX = true
|
|
121
|
+
kv += struct.pack("<IB", 0x20910008, 1) # CFG-MSGOUT-UBX_NAV_PVT_UART2 = 1
|
|
122
|
+
payload = struct.pack("<BBH", 0x00, 0x07, 0x0000) + kv
|
|
123
|
+
frame = _ubx_frame(0x06, 0x8A, payload)
|
|
124
|
+
self._writer.write(frame)
|
|
125
|
+
await self._writer.drain()
|
|
126
|
+
await asyncio.sleep(0.2)
|
|
127
|
+
logger.info("UBX UART configured")
|
|
128
|
+
|
|
129
|
+
def _poll_pvt(self) -> None:
|
|
130
|
+
if self._writer is not None:
|
|
131
|
+
self._writer.write(NAV_PVT_POLL)
|
|
132
|
+
|
|
133
|
+
async def read_pvt(self) -> UBXNavPVT | None:
|
|
134
|
+
if self._reader is None:
|
|
135
|
+
raise RuntimeError("call start() first")
|
|
136
|
+
|
|
137
|
+
self._poll_pvt()
|
|
138
|
+
loop = asyncio.get_running_loop()
|
|
139
|
+
deadline = loop.time() + READ_PVT_TIMEOUT_S
|
|
140
|
+
|
|
141
|
+
while True:
|
|
142
|
+
remaining = deadline - loop.time()
|
|
143
|
+
if remaining <= 0:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
while len(self._buf) < 2:
|
|
147
|
+
chunk = await self._read_chunk(4096, remaining)
|
|
148
|
+
if not chunk:
|
|
149
|
+
return None
|
|
150
|
+
self._buf.extend(chunk)
|
|
151
|
+
remaining = deadline - loop.time()
|
|
152
|
+
if remaining <= 0:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
sync_at = self._buf.find(bytes([UBX_SYNC_1, UBX_SYNC_2]))
|
|
156
|
+
if sync_at < 0:
|
|
157
|
+
if len(self._buf) > 1:
|
|
158
|
+
del self._buf[:-1]
|
|
159
|
+
elif len(self._buf) == 1 and self._buf[0] != UBX_SYNC_1:
|
|
160
|
+
self._buf.clear()
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
if sync_at > 0:
|
|
164
|
+
del self._buf[:sync_at]
|
|
165
|
+
|
|
166
|
+
need = 6
|
|
167
|
+
while len(self._buf) < need:
|
|
168
|
+
chunk = await self._read_chunk(need - len(self._buf), remaining)
|
|
169
|
+
if not chunk:
|
|
170
|
+
return None
|
|
171
|
+
self._buf.extend(chunk)
|
|
172
|
+
remaining = deadline - loop.time()
|
|
173
|
+
if remaining <= 0:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
payload_len = struct.unpack_from("<H", self._buf, 4)[0]
|
|
177
|
+
frame_len = 6 + payload_len + 2
|
|
178
|
+
|
|
179
|
+
while len(self._buf) < frame_len:
|
|
180
|
+
chunk = await self._read_chunk(frame_len - len(self._buf), remaining)
|
|
181
|
+
if not chunk:
|
|
182
|
+
return None
|
|
183
|
+
self._buf.extend(chunk)
|
|
184
|
+
remaining = deadline - loop.time()
|
|
185
|
+
if remaining <= 0:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
body = bytes(self._buf[2:6 + payload_len])
|
|
189
|
+
ck_a, ck_b = self._buf[6 + payload_len], self._buf[6 + payload_len + 1]
|
|
190
|
+
exp_a, exp_b = _ubx_checksum(body)
|
|
191
|
+
|
|
192
|
+
if ck_a != exp_a or ck_b != exp_b:
|
|
193
|
+
del self._buf[0]
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
del self._buf[:frame_len]
|
|
197
|
+
|
|
198
|
+
msg_class = body[0]
|
|
199
|
+
msg_id = body[1]
|
|
200
|
+
if (
|
|
201
|
+
msg_class == NAV_CLASS
|
|
202
|
+
and msg_id == NAV_PVT_ID
|
|
203
|
+
and payload_len == NAV_PVT_PAYLOAD_LEN
|
|
204
|
+
):
|
|
205
|
+
try:
|
|
206
|
+
return _parse_nav_pvt_payload(body[4:])
|
|
207
|
+
except ValueError:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
async def _read_chunk(self, max_bytes: int, timeout_s: float) -> bytes | None:
|
|
211
|
+
if self._reader is None:
|
|
212
|
+
return None
|
|
213
|
+
try:
|
|
214
|
+
return await asyncio.wait_for(
|
|
215
|
+
self._reader.read(max(max_bytes, 1)), timeout=timeout_s
|
|
216
|
+
)
|
|
217
|
+
except asyncio.TimeoutError:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
async def stop(self) -> None:
|
|
221
|
+
if self._writer is not None:
|
|
222
|
+
self._writer.close()
|
|
223
|
+
try:
|
|
224
|
+
await self._writer.wait_closed()
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
self._writer = None
|
|
228
|
+
self._reader = None
|
|
229
|
+
self._buf.clear()
|
|
230
|
+
logger.info("UBX reader stopped", port=self._port)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hubble-gateway-sdk-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for building Hubble Network BLE gateways
|
|
5
|
+
Project-URL: Homepage, https://github.com/HubbleNetwork/gateway-sdk-python
|
|
6
|
+
Project-URL: Documentation, https://github.com/HubbleNetwork/gateway-sdk-python#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/HubbleNetwork/gateway-sdk-python
|
|
8
|
+
Project-URL: Issues, https://github.com/HubbleNetwork/gateway-sdk-python/issues
|
|
9
|
+
Author-email: Hubble Network <dev@hubblenetwork.com>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ble,bluetooth,gateway,hubble,iot,sdk
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Home Automation
|
|
21
|
+
Classifier: Topic :: System :: Networking
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: bleak>=0.21.0
|
|
24
|
+
Requires-Dist: httpx>=0.25.0
|
|
25
|
+
Requires-Dist: pydantic-settings>=2.1.0
|
|
26
|
+
Requires-Dist: pydantic>=2.5.0
|
|
27
|
+
Requires-Dist: structlog>=23.2.0
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: gpsd-py3>=0.3.0; extra == 'all'
|
|
30
|
+
Requires-Dist: pyserial-asyncio>=0.6; extra == 'all'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: gpsd-py3>=0.3.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pyserial-asyncio>=0.6; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
37
|
+
Provides-Extra: gps
|
|
38
|
+
Requires-Dist: gpsd-py3>=0.3.0; extra == 'gps'
|
|
39
|
+
Requires-Dist: pyserial-asyncio>=0.6; extra == 'gps'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# hubble-gateway
|
|
43
|
+
|
|
44
|
+
Python SDK for building Hubble Network BLE gateways. Import `hubble_gateway` in your application; this package is a library only (no CLI).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
hubble_gateway/__init__.py,sha256=yEpF8M0DMCqWkeRKYEzb4HRCj7qTdoXxjS2c4cnMkZ0,1148
|
|
2
|
+
hubble_gateway/auth.py,sha256=WVlSeuVYpkGK0I_DGRfxxx1U0HKOG47zMnwaRkefLIE,9137
|
|
3
|
+
hubble_gateway/config.py,sha256=OdEFHISltqQpYSJLIQa2k_jdG57s_aJaPTxMqfPS2v4,3135
|
|
4
|
+
hubble_gateway/location.py,sha256=RQbQWfyBv7UPCenCqR6F_vIIvFof-4AdvPfaa0ws8t8,14862
|
|
5
|
+
hubble_gateway/models.py,sha256=vQHGuC2RD5ph5d-Pj9G6kPurl89PKCoh_PBRDBZEXJs,4212
|
|
6
|
+
hubble_gateway/scanner.py,sha256=W5NXt_pHBawO0yREwtnOP4LkVO6pxkl6Z-UQm3hopko,5269
|
|
7
|
+
hubble_gateway/sender.py,sha256=61QlSMmATZKAJns3_0WkNOm7Tqlz3II5eGQnbBrvjfQ,7166
|
|
8
|
+
hubble_gateway/ubx.py,sha256=_PCfqAbkVsB-fKjR9kMTSHpYZXX9sf-PufDICBC5JUI,7424
|
|
9
|
+
hubble_gateway_sdk_python-0.1.0.dist-info/METADATA,sha256=iewTW8xgLOIdMqLAr-YIgqe5nTXjyS7Cq1KrwzPe7Tg,1915
|
|
10
|
+
hubble_gateway_sdk_python-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
11
|
+
hubble_gateway_sdk_python-0.1.0.dist-info/licenses/LICENSE,sha256=L6iQZR753mEQy5DSX2zByjAG1GlwCKrZDk5lifagvDI,11397
|
|
12
|
+
hubble_gateway_sdk_python-0.1.0.dist-info/RECORD,,
|