spelliot 1.0.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.
- spelliot-1.0.0/PKG-INFO +17 -0
- spelliot-1.0.0/setup.cfg +4 -0
- spelliot-1.0.0/setup.py +19 -0
- spelliot-1.0.0/spell_iot.py +208 -0
- spelliot-1.0.0/spelliot.egg-info/PKG-INFO +17 -0
- spelliot-1.0.0/spelliot.egg-info/SOURCES.txt +7 -0
- spelliot-1.0.0/spelliot.egg-info/dependency_links.txt +1 -0
- spelliot-1.0.0/spelliot.egg-info/requires.txt +2 -0
- spelliot-1.0.0/spelliot.egg-info/top_level.txt +1 -0
spelliot-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spelliot
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Spell IoT Cloud Library for Python and Raspberry Pi
|
|
5
|
+
Author: Karthickraja (Petals Automation)
|
|
6
|
+
Author-email: petalsautomationembedded@gmail.com
|
|
7
|
+
Maintainer: Petals Automation
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Dist: websocket-client
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: author-email
|
|
14
|
+
Dynamic: classifier
|
|
15
|
+
Dynamic: maintainer
|
|
16
|
+
Dynamic: requires-dist
|
|
17
|
+
Dynamic: summary
|
spelliot-1.0.0/setup.cfg
ADDED
spelliot-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='spelliot',
|
|
5
|
+
version='1.0.0',
|
|
6
|
+
description='Spell IoT Cloud Library for Python and Raspberry Pi',
|
|
7
|
+
author='Karthickraja (Petals Automation)',
|
|
8
|
+
maintainer='Petals Automation',
|
|
9
|
+
author_email='petalsautomationembedded@gmail.com',
|
|
10
|
+
py_modules=['spell_iot'], # This tells pip to include spell_iot.py
|
|
11
|
+
install_requires=[
|
|
12
|
+
'websocket-client',
|
|
13
|
+
'requests'
|
|
14
|
+
],
|
|
15
|
+
classifiers=[
|
|
16
|
+
'Programming Language :: Python :: 3',
|
|
17
|
+
'Operating System :: OS Independent',
|
|
18
|
+
],
|
|
19
|
+
)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import websocket
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import threading
|
|
5
|
+
import urllib.parse
|
|
6
|
+
|
|
7
|
+
class Spell_IoT:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.ssid = ""
|
|
10
|
+
self.password = ""
|
|
11
|
+
self.device_token = ""
|
|
12
|
+
self.callbacks = {}
|
|
13
|
+
self.last_values = {}
|
|
14
|
+
self.last_rgb = {}
|
|
15
|
+
self.ws = None
|
|
16
|
+
self.ws_url = "wss://api.spelliot.com:443/ws-mobile"
|
|
17
|
+
self._connected = False
|
|
18
|
+
|
|
19
|
+
def begin(self, token, ssid="", password=""):
|
|
20
|
+
# ssid and password are not strictly needed on Raspberry Pi
|
|
21
|
+
# as it connects to the network via OS, but kept for compatibility with C++.
|
|
22
|
+
self.ssid = ssid
|
|
23
|
+
self.password = password
|
|
24
|
+
self.device_token = token
|
|
25
|
+
|
|
26
|
+
# Start the status thread
|
|
27
|
+
self._status_thread = threading.Thread(target=self._status_thread_func)
|
|
28
|
+
self._status_thread.daemon = True
|
|
29
|
+
self._status_thread.start()
|
|
30
|
+
|
|
31
|
+
def registerPin(self, pin, callback):
|
|
32
|
+
self.callbacks[pin.upper()] = callback
|
|
33
|
+
|
|
34
|
+
# --- READ FUNCTIONS ---
|
|
35
|
+
def read(self, pin):
|
|
36
|
+
return self.last_values.get(pin.upper(), "")
|
|
37
|
+
|
|
38
|
+
def readInt(self, pin):
|
|
39
|
+
val = self.read(pin)
|
|
40
|
+
try:
|
|
41
|
+
return int(val)
|
|
42
|
+
except ValueError:
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
def readBool(self, pin):
|
|
46
|
+
return self.read(pin) == "1"
|
|
47
|
+
|
|
48
|
+
def readRGB(self, pin):
|
|
49
|
+
# Returns a tuple (R, G, B)
|
|
50
|
+
return self.last_rgb.get(pin.upper(), (0, 0, 0))
|
|
51
|
+
|
|
52
|
+
# --- WRITE FUNCTIONS ---
|
|
53
|
+
def urlEncode(self, value):
|
|
54
|
+
return urllib.parse.quote(str(value))
|
|
55
|
+
|
|
56
|
+
def writeAck(self, pin, value):
|
|
57
|
+
if not self._connected or not self.ws:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
pin = pin.upper()
|
|
61
|
+
encodedValue = self.urlEncode(value)
|
|
62
|
+
fm = "0.0.0" # Firmware version equivalent
|
|
63
|
+
msg = (
|
|
64
|
+
"SEND\n"
|
|
65
|
+
f"destination:/app/device/{self.device_token}\n\n"
|
|
66
|
+
f"{pin}={encodedValue}&version={fm}\0"
|
|
67
|
+
)
|
|
68
|
+
self.ws.send(msg)
|
|
69
|
+
print(msg)
|
|
70
|
+
print(f"Data Sent -> Pin: {pin} | Value: {value}")
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
def write(self, pin, value):
|
|
74
|
+
# Wrapper for writeAck just like in C++
|
|
75
|
+
return self.writeAck(pin, value)
|
|
76
|
+
|
|
77
|
+
def writeInternal(self, pin, value):
|
|
78
|
+
return self.writeAck(pin, value)
|
|
79
|
+
|
|
80
|
+
def writeRawWS(self, pin, value):
|
|
81
|
+
if self.ws and self._connected:
|
|
82
|
+
self.ws.send(str(value))
|
|
83
|
+
|
|
84
|
+
def Status(self):
|
|
85
|
+
return self._connected
|
|
86
|
+
|
|
87
|
+
def _status_thread_func(self):
|
|
88
|
+
while True:
|
|
89
|
+
if self._connected:
|
|
90
|
+
self.write("status", "Online")
|
|
91
|
+
time.sleep(10)
|
|
92
|
+
|
|
93
|
+
# --- SKYLINK DOCKER MEMORY (Stub for Pi) ---
|
|
94
|
+
def storeMemoryString(self, keyss, values):
|
|
95
|
+
print(f"[Docker Memory] Saving {keyss} = {values}")
|
|
96
|
+
|
|
97
|
+
def storeMemoryInt(self, keyss, values):
|
|
98
|
+
print(f"[Docker Memory] Saving {keyss} = {values}")
|
|
99
|
+
|
|
100
|
+
# --- OTA UPDATES (Stub for Pi) ---
|
|
101
|
+
def updates(self, url):
|
|
102
|
+
print(f"[SpellIoT Air Update] Update requested from URL: {url}")
|
|
103
|
+
print("Note: Air Updates (OTA) work differently on Raspberry Pi than ESP32.")
|
|
104
|
+
print("You can implement Python file downloading and script restarting here.")
|
|
105
|
+
|
|
106
|
+
# --- INTERNAL HELPERS ---
|
|
107
|
+
def _hex_to_rgb(self, hex_str):
|
|
108
|
+
hex_str = hex_str.lstrip('#')
|
|
109
|
+
if len(hex_str) != 6:
|
|
110
|
+
return (0, 0, 0)
|
|
111
|
+
try:
|
|
112
|
+
return tuple(int(hex_str[i:i+2], 16) for i in (0, 2, 4))
|
|
113
|
+
except:
|
|
114
|
+
return (0, 0, 0)
|
|
115
|
+
|
|
116
|
+
# --- WEBSOCKET EVENT HANDLERS ---
|
|
117
|
+
def _on_message(self, ws, message):
|
|
118
|
+
if message.startswith("CONNECTED"):
|
|
119
|
+
print("Spell-IoT Cloud CONNECTED")
|
|
120
|
+
self._connected = True
|
|
121
|
+
subscribe_frame = (
|
|
122
|
+
"SUBSCRIBE\n"
|
|
123
|
+
"id:sub-0\n"
|
|
124
|
+
"ack:auto\n"
|
|
125
|
+
f"destination:/topic/device/{self.device_token}\n\n\0"
|
|
126
|
+
)
|
|
127
|
+
ws.send(subscribe_frame)
|
|
128
|
+
|
|
129
|
+
elif message.startswith("MESSAGE"):
|
|
130
|
+
try:
|
|
131
|
+
parts = message.split("\n\n")
|
|
132
|
+
if len(parts) > 1:
|
|
133
|
+
body = parts[1].strip('\0')
|
|
134
|
+
|
|
135
|
+
# Prevent JSON decode error on empty STOMP frames (like heartbeat responses)
|
|
136
|
+
if not body.strip():
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
data = json.loads(body)
|
|
140
|
+
pin = data.get("pin", "").strip().upper()
|
|
141
|
+
val = data.get("value", "").strip()
|
|
142
|
+
|
|
143
|
+
print(f"Data Received <- Pin: {pin} | Value: {val}")
|
|
144
|
+
|
|
145
|
+
if pin == "AIR":
|
|
146
|
+
print("SpellIoT Air Update!")
|
|
147
|
+
self.updates(val)
|
|
148
|
+
else:
|
|
149
|
+
# Store cache
|
|
150
|
+
self.last_values[pin] = val
|
|
151
|
+
if val.startswith("#"):
|
|
152
|
+
self.last_rgb[pin] = self._hex_to_rgb(val)
|
|
153
|
+
|
|
154
|
+
# Call user callback if registered
|
|
155
|
+
if pin in self.callbacks:
|
|
156
|
+
self.callbacks[pin](val)
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
# Suppress the known empty body JSON error completely
|
|
160
|
+
if "Expecting value: line 1 column 1" not in str(e):
|
|
161
|
+
print(f"WS Error parsing: {e}")
|
|
162
|
+
|
|
163
|
+
def _on_error(self, ws, error):
|
|
164
|
+
print(f"WS Error: {error}")
|
|
165
|
+
|
|
166
|
+
def _on_close(self, ws, close_status_code, close_msg):
|
|
167
|
+
print("WS Disconnected!")
|
|
168
|
+
self._connected = False
|
|
169
|
+
|
|
170
|
+
def _on_open(self, ws):
|
|
171
|
+
connect_frame = (
|
|
172
|
+
"CONNECT\n"
|
|
173
|
+
"accept-version:1.2\n"
|
|
174
|
+
"host:api.spelliot.com\n"
|
|
175
|
+
"heart-beat:10000,0000\n\n\0"
|
|
176
|
+
)
|
|
177
|
+
ws.send(connect_frame)
|
|
178
|
+
|
|
179
|
+
def _run_ws(self):
|
|
180
|
+
while True:
|
|
181
|
+
self.ws = websocket.WebSocketApp(self.ws_url,
|
|
182
|
+
on_open=self._on_open,
|
|
183
|
+
on_message=self._on_message,
|
|
184
|
+
on_error=self._on_error,
|
|
185
|
+
on_close=self._on_close)
|
|
186
|
+
self.ws.run_forever()
|
|
187
|
+
print("Reconnecting in 5 seconds...")
|
|
188
|
+
time.sleep(5)
|
|
189
|
+
|
|
190
|
+
# --- LOOP FUNCTIONS ---
|
|
191
|
+
def autoRun(self):
|
|
192
|
+
"""Starts the websocket client in a background thread (same as loop_start)"""
|
|
193
|
+
self.loop_start()
|
|
194
|
+
|
|
195
|
+
def loop_start(self):
|
|
196
|
+
"""Starts the websocket client in a background thread"""
|
|
197
|
+
thread = threading.Thread(target=self._run_ws)
|
|
198
|
+
thread.daemon = True
|
|
199
|
+
thread.start()
|
|
200
|
+
|
|
201
|
+
def loop_forever(self):
|
|
202
|
+
"""Starts the websocket client and blocks forever"""
|
|
203
|
+
self._run_ws()
|
|
204
|
+
|
|
205
|
+
def loop(self):
|
|
206
|
+
"""Equivalent to C++ loop(), does nothing in Python since thread is already running"""
|
|
207
|
+
pass
|
|
208
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spelliot
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Spell IoT Cloud Library for Python and Raspberry Pi
|
|
5
|
+
Author: Karthickraja (Petals Automation)
|
|
6
|
+
Author-email: petalsautomationembedded@gmail.com
|
|
7
|
+
Maintainer: Petals Automation
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Dist: websocket-client
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: author-email
|
|
14
|
+
Dynamic: classifier
|
|
15
|
+
Dynamic: maintainer
|
|
16
|
+
Dynamic: requires-dist
|
|
17
|
+
Dynamic: summary
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spell_iot
|