iotfiy 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.
- iotfiy-1.0.0/PKG-INFO +10 -0
- iotfiy-1.0.0/pyproject.toml +20 -0
- iotfiy-1.0.0/setup.cfg +4 -0
- iotfiy-1.0.0/src/iotfiy/__init__.py +7 -0
- iotfiy-1.0.0/src/iotfiy/client.py +367 -0
- iotfiy-1.0.0/src/iotfiy/device_config.py +96 -0
- iotfiy-1.0.0/src/iotfiy/receive.py +124 -0
- iotfiy-1.0.0/src/iotfiy/ref.py +21 -0
- iotfiy-1.0.0/src/iotfiy/send.py +102 -0
- iotfiy-1.0.0/src/iotfiy.egg-info/PKG-INFO +10 -0
- iotfiy-1.0.0/src/iotfiy.egg-info/SOURCES.txt +12 -0
- iotfiy-1.0.0/src/iotfiy.egg-info/dependency_links.txt +1 -0
- iotfiy-1.0.0/src/iotfiy.egg-info/requires.txt +4 -0
- iotfiy-1.0.0/src/iotfiy.egg-info/top_level.txt +1 -0
iotfiy-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iotfiy
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: IOTFIY MQTT gateway client for Raspberry Pi 3/4/5
|
|
5
|
+
Author-email: Iotfiy Solutions <iotfiy.solution@gmail.com>
|
|
6
|
+
Keywords: iot,mqtt,raspberry-pi,iotfiy
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: paho-mqtt>=1.6.1
|
|
9
|
+
Provides-Extra: gpio
|
|
10
|
+
Requires-Dist: gpiozero; extra == "gpio"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "iotfiy"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "IOTFIY MQTT gateway client for Raspberry Pi 3/4/5"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
authors = [{ name = "Iotfiy Solutions", email = "iotfiy.solution@gmail.com" }]
|
|
11
|
+
keywords = ["iot", "mqtt", "raspberry-pi", "iotfiy"]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"paho-mqtt>=1.6.1",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
gpio = ["gpiozero"]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["src"]
|
iotfiy-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""IOTFIY manager - Arduino IOTFIYManager API for Raspberry Pi."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import socket
|
|
7
|
+
import time
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
import paho.mqtt.client as mqtt
|
|
11
|
+
|
|
12
|
+
from .device_config import DeviceConfig
|
|
13
|
+
from .receive import Receive
|
|
14
|
+
from .ref import Ref
|
|
15
|
+
from .send import Send
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IOTFIYManager:
|
|
19
|
+
MAX_SENDS = 10
|
|
20
|
+
MAX_RECEIVES = 10
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self._sends: List[Send] = []
|
|
24
|
+
self._receives: List[Receive] = []
|
|
25
|
+
self._config = DeviceConfig()
|
|
26
|
+
self._lwt_enabled = False
|
|
27
|
+
self._client: Optional[mqtt.Client] = None
|
|
28
|
+
|
|
29
|
+
def config_obj(self) -> DeviceConfig:
|
|
30
|
+
"""Access underlying DeviceConfig (Arduino: config())."""
|
|
31
|
+
return self._config
|
|
32
|
+
|
|
33
|
+
def enable_lwt(self, enabled: bool = True) -> None:
|
|
34
|
+
self._lwt_enabled = enabled
|
|
35
|
+
state = "ENABLED (offline on disconnect)" if enabled else "disabled"
|
|
36
|
+
print(f"MQTT LWT {state}")
|
|
37
|
+
|
|
38
|
+
def config(
|
|
39
|
+
self,
|
|
40
|
+
ssid: str,
|
|
41
|
+
password: str,
|
|
42
|
+
mqtt_server: str,
|
|
43
|
+
mqtt_client_id: str,
|
|
44
|
+
device_id: str,
|
|
45
|
+
mqtt_port: int = 1883,
|
|
46
|
+
mqtt_username: str = "",
|
|
47
|
+
mqtt_password: str = "",
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Same signature as Arduino IOTFIY.config(...).
|
|
50
|
+
|
|
51
|
+
On Raspberry Pi, WiFi is normally managed by the OS. ``ssid`` /
|
|
52
|
+
``password`` are kept for API compatibility; MQTT settings are required.
|
|
53
|
+
"""
|
|
54
|
+
self._config.ssid(ssid).password(password).server(mqtt_server).port(
|
|
55
|
+
mqtt_port
|
|
56
|
+
).client_id(mqtt_client_id).device_id(device_id).mqtt_username(
|
|
57
|
+
mqtt_username
|
|
58
|
+
).mqtt_password(mqtt_password)
|
|
59
|
+
|
|
60
|
+
def begin(self) -> None:
|
|
61
|
+
if not self._config.is_valid():
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"IOTFIY config incomplete - set mqtt_server, client_id, device_id"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# paho-mqtt 2.x uses CallbackAPIVersion; 1.x does not.
|
|
67
|
+
try:
|
|
68
|
+
self._client = mqtt.Client(
|
|
69
|
+
mqtt.CallbackAPIVersion.VERSION1,
|
|
70
|
+
client_id=self._config.get_mqtt_client_id,
|
|
71
|
+
)
|
|
72
|
+
except (AttributeError, TypeError):
|
|
73
|
+
self._client = mqtt.Client(client_id=self._config.get_mqtt_client_id)
|
|
74
|
+
|
|
75
|
+
self._client.on_message = self._on_message
|
|
76
|
+
self._client.on_disconnect = self._on_disconnect
|
|
77
|
+
|
|
78
|
+
if self._config.get_mqtt_username:
|
|
79
|
+
self._client.username_pw_set(
|
|
80
|
+
self._config.get_mqtt_username,
|
|
81
|
+
self._config.get_mqtt_password,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if self._lwt_enabled:
|
|
85
|
+
will_payload = json.dumps(
|
|
86
|
+
{
|
|
87
|
+
"status": "offline",
|
|
88
|
+
"deviceId": self._config.get_device_id,
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
self._client.will_set(
|
|
92
|
+
self._config.get_status_topic(),
|
|
93
|
+
payload=will_payload,
|
|
94
|
+
qos=1,
|
|
95
|
+
retain=True,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self._bind_client_to_capabilities()
|
|
99
|
+
|
|
100
|
+
def register_all_capabilities(self) -> None:
|
|
101
|
+
start = {
|
|
102
|
+
"deviceId": self._config.get_device_id,
|
|
103
|
+
"event": "capabilities_registration_start",
|
|
104
|
+
"total_controls": len(self._receives),
|
|
105
|
+
"total_sensors": len(self._sends),
|
|
106
|
+
}
|
|
107
|
+
self._publish(self._config.get_status_topic(), start, retain=False)
|
|
108
|
+
|
|
109
|
+
for receive in self._receives:
|
|
110
|
+
receive.register_capability()
|
|
111
|
+
for send in self._sends:
|
|
112
|
+
send.register_capability()
|
|
113
|
+
|
|
114
|
+
end = {
|
|
115
|
+
"deviceId": self._config.get_device_id,
|
|
116
|
+
"event": "capabilities_registration_complete",
|
|
117
|
+
"controls_registered": len(self._receives),
|
|
118
|
+
"sensors_registered": len(self._sends),
|
|
119
|
+
}
|
|
120
|
+
self._publish(self._config.get_status_topic(), end, retain=False)
|
|
121
|
+
|
|
122
|
+
def launch(self) -> None:
|
|
123
|
+
for send in self._sends:
|
|
124
|
+
send.publish_data()
|
|
125
|
+
|
|
126
|
+
def publish_string_output(self, name: str, pin: str, value: str) -> bool:
|
|
127
|
+
if not self.is_mqtt_connected():
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
cap = {
|
|
131
|
+
"deviceId": self._config.get_device_id,
|
|
132
|
+
"type": "capabilities_output",
|
|
133
|
+
"outputName": name,
|
|
134
|
+
"output": {"type": "string", "pin": pin},
|
|
135
|
+
}
|
|
136
|
+
cap_topic = (
|
|
137
|
+
f"devices/{self._config.get_device_id}/capabilities_output/{name}"
|
|
138
|
+
)
|
|
139
|
+
self._publish(cap_topic, cap, retain=True)
|
|
140
|
+
|
|
141
|
+
data = {
|
|
142
|
+
"type": "output",
|
|
143
|
+
"deviceId": self._config.get_device_id,
|
|
144
|
+
"data": {name: value, pin: value},
|
|
145
|
+
}
|
|
146
|
+
print(f"String output {pin}={value}")
|
|
147
|
+
return self._publish(self._config.get_data_topic(), data, retain=False)
|
|
148
|
+
|
|
149
|
+
def reconnect_mqtt(self) -> bool:
|
|
150
|
+
if self._client is None:
|
|
151
|
+
self.begin()
|
|
152
|
+
|
|
153
|
+
assert self._client is not None
|
|
154
|
+
if self._client.is_connected():
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
for attempt in range(1, 6):
|
|
158
|
+
print(f"MQTT connection attempt {attempt}... ", end="", flush=True)
|
|
159
|
+
try:
|
|
160
|
+
self._client.connect(
|
|
161
|
+
self._config.get_mqtt_server,
|
|
162
|
+
self._config.get_mqtt_port,
|
|
163
|
+
keepalive=60,
|
|
164
|
+
)
|
|
165
|
+
# Give the network stack a moment, then poll until connected.
|
|
166
|
+
deadline = time.time() + 8
|
|
167
|
+
while time.time() < deadline and not self._client.is_connected():
|
|
168
|
+
self._client.loop(timeout=0.2)
|
|
169
|
+
|
|
170
|
+
if self._client.is_connected():
|
|
171
|
+
print("connected!")
|
|
172
|
+
if self._lwt_enabled:
|
|
173
|
+
print(f"LWT registered on {self._config.get_status_topic()}")
|
|
174
|
+
|
|
175
|
+
self._client.subscribe(self._config.get_commands_topic())
|
|
176
|
+
status = {
|
|
177
|
+
"status": "online",
|
|
178
|
+
"deviceId": self._config.get_device_id,
|
|
179
|
+
}
|
|
180
|
+
self._publish(
|
|
181
|
+
self._config.get_status_topic(), status, retain=True
|
|
182
|
+
)
|
|
183
|
+
self.register_all_capabilities()
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
print("failed (timeout)")
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
print(f"failed ({exc})")
|
|
189
|
+
|
|
190
|
+
if attempt < 5:
|
|
191
|
+
time.sleep(5)
|
|
192
|
+
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
def check_and_reconnect_wifi(self) -> bool:
|
|
196
|
+
"""On Pi, verify network path to the MQTT broker (OS manages Wi-Fi)."""
|
|
197
|
+
host = self._config.get_mqtt_server
|
|
198
|
+
port = self._config.get_mqtt_port
|
|
199
|
+
if not host:
|
|
200
|
+
print("WiFi/network check failed: MQTT server not configured")
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
with socket.create_connection((host, port), timeout=3):
|
|
205
|
+
return True
|
|
206
|
+
except OSError:
|
|
207
|
+
print("Network unreachable to MQTT broker. Will retry...")
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
def is_mqtt_connected(self) -> bool:
|
|
211
|
+
return bool(self._client and self._client.is_connected())
|
|
212
|
+
|
|
213
|
+
def process_mqtt_messages(self) -> None:
|
|
214
|
+
if self._client is not None:
|
|
215
|
+
self._client.loop(timeout=0.05)
|
|
216
|
+
|
|
217
|
+
def write(
|
|
218
|
+
self,
|
|
219
|
+
name: str,
|
|
220
|
+
type_: str,
|
|
221
|
+
pin: str,
|
|
222
|
+
data_source: Ref,
|
|
223
|
+
min_value: Optional[float] = None,
|
|
224
|
+
max_value: Optional[float] = None,
|
|
225
|
+
) -> Optional[Send]:
|
|
226
|
+
if len(self._sends) >= self.MAX_SENDS:
|
|
227
|
+
return None
|
|
228
|
+
if not isinstance(data_source, Ref):
|
|
229
|
+
raise TypeError("write() expects an iotfiy.Ref for data_source")
|
|
230
|
+
|
|
231
|
+
send = Send(
|
|
232
|
+
name,
|
|
233
|
+
type_,
|
|
234
|
+
pin,
|
|
235
|
+
data_source,
|
|
236
|
+
self._client,
|
|
237
|
+
self._config,
|
|
238
|
+
min_value,
|
|
239
|
+
max_value,
|
|
240
|
+
)
|
|
241
|
+
# Client may be created later in begin(); keep reference alive.
|
|
242
|
+
self._sends.append(send)
|
|
243
|
+
self._bind_client_to_capabilities()
|
|
244
|
+
return send
|
|
245
|
+
|
|
246
|
+
def read(
|
|
247
|
+
self,
|
|
248
|
+
name: str,
|
|
249
|
+
type_: str,
|
|
250
|
+
pin: str,
|
|
251
|
+
entry_point: Ref,
|
|
252
|
+
min_value: Optional[float] = None,
|
|
253
|
+
max_value: Optional[float] = None,
|
|
254
|
+
) -> Optional[Receive]:
|
|
255
|
+
if len(self._receives) >= self.MAX_RECEIVES:
|
|
256
|
+
return None
|
|
257
|
+
if not isinstance(entry_point, Ref):
|
|
258
|
+
raise TypeError("read() expects an iotfiy.Ref for entry_point")
|
|
259
|
+
|
|
260
|
+
receive = Receive(
|
|
261
|
+
name,
|
|
262
|
+
type_,
|
|
263
|
+
pin,
|
|
264
|
+
entry_point,
|
|
265
|
+
self._client,
|
|
266
|
+
self._config,
|
|
267
|
+
min_value,
|
|
268
|
+
max_value,
|
|
269
|
+
)
|
|
270
|
+
self._receives.append(receive)
|
|
271
|
+
self._bind_client_to_capabilities()
|
|
272
|
+
return receive
|
|
273
|
+
|
|
274
|
+
def _bind_client_to_capabilities(self) -> None:
|
|
275
|
+
if self._client is None:
|
|
276
|
+
return
|
|
277
|
+
for send in self._sends:
|
|
278
|
+
send._client = self._client
|
|
279
|
+
for receive in self._receives:
|
|
280
|
+
receive._client = self._client
|
|
281
|
+
|
|
282
|
+
def _publish(self, topic: str, payload: dict, retain: bool) -> bool:
|
|
283
|
+
if not self.is_mqtt_connected():
|
|
284
|
+
return False
|
|
285
|
+
assert self._client is not None
|
|
286
|
+
try:
|
|
287
|
+
info = self._client.publish(topic, json.dumps(payload), retain=retain)
|
|
288
|
+
return info.rc == 0
|
|
289
|
+
except Exception as exc:
|
|
290
|
+
print(f"MQTT publish error: {exc}")
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
def _on_disconnect(self, client, userdata, rc):
|
|
294
|
+
if rc != 0:
|
|
295
|
+
print(f"Unexpected MQTT disconnect (rc={rc})")
|
|
296
|
+
|
|
297
|
+
def _on_message(self, client, userdata, msg):
|
|
298
|
+
try:
|
|
299
|
+
message = msg.payload.decode("utf-8", errors="replace")
|
|
300
|
+
except Exception:
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
print(f"Message arrived [{msg.topic}] {message}")
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
doc = json.loads(message)
|
|
307
|
+
except json.JSONDecodeError as exc:
|
|
308
|
+
print(f"JSON parse failed: {exc}")
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
if msg.topic != self._config.get_commands_topic():
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
value = None
|
|
315
|
+
control_name = None
|
|
316
|
+
pin_name = None
|
|
317
|
+
|
|
318
|
+
if isinstance(doc, dict) and "data" in doc and "type" in doc:
|
|
319
|
+
data = doc.get("data") or {}
|
|
320
|
+
value = data.get("value")
|
|
321
|
+
control_name = data.get("control")
|
|
322
|
+
pin_name = data.get("pin")
|
|
323
|
+
elif isinstance(doc, dict):
|
|
324
|
+
value = doc.get("value")
|
|
325
|
+
control_name = doc.get("control")
|
|
326
|
+
pin_name = doc.get("pin")
|
|
327
|
+
|
|
328
|
+
target: Optional[Receive] = None
|
|
329
|
+
if control_name:
|
|
330
|
+
for receive in self._receives:
|
|
331
|
+
if receive.get_name() == control_name:
|
|
332
|
+
target = receive
|
|
333
|
+
break
|
|
334
|
+
elif pin_name:
|
|
335
|
+
for receive in self._receives:
|
|
336
|
+
if receive.get_pin() == pin_name:
|
|
337
|
+
target = receive
|
|
338
|
+
break
|
|
339
|
+
|
|
340
|
+
if target is not None:
|
|
341
|
+
result = {}
|
|
342
|
+
success = target.handle_command(value, result)
|
|
343
|
+
response = {
|
|
344
|
+
"status": "success" if success else "error",
|
|
345
|
+
"control": target.get_name(),
|
|
346
|
+
"result": result,
|
|
347
|
+
}
|
|
348
|
+
self._publish(
|
|
349
|
+
self._config.get_command_response_topic(), response, retain=False
|
|
350
|
+
)
|
|
351
|
+
else:
|
|
352
|
+
print("ERROR: No matching control or pin found")
|
|
353
|
+
error = {
|
|
354
|
+
"status": "error",
|
|
355
|
+
"message": "No matching control or pin found",
|
|
356
|
+
}
|
|
357
|
+
if control_name:
|
|
358
|
+
error["control"] = control_name
|
|
359
|
+
if pin_name:
|
|
360
|
+
error["pin"] = pin_name
|
|
361
|
+
self._publish(
|
|
362
|
+
self._config.get_command_response_topic(), error, retain=False
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# Global instance (Arduino: extern IOTFIYManager IOTFIY)
|
|
367
|
+
IOTFIY = IOTFIYManager()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Device / MQTT configuration (mirrors Arduino DeviceConfig)."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DeviceConfig:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self._ssid = ""
|
|
7
|
+
self._password = ""
|
|
8
|
+
self._mqtt_server = ""
|
|
9
|
+
self._mqtt_port = 1883
|
|
10
|
+
self._mqtt_client_id = ""
|
|
11
|
+
self._mqtt_username = ""
|
|
12
|
+
self._mqtt_password = ""
|
|
13
|
+
self._device_id = ""
|
|
14
|
+
|
|
15
|
+
def ssid(self, value: str):
|
|
16
|
+
self._ssid = value or ""
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
def password(self, value: str):
|
|
20
|
+
self._password = value or ""
|
|
21
|
+
return self
|
|
22
|
+
|
|
23
|
+
def server(self, value: str):
|
|
24
|
+
self._mqtt_server = value or ""
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
def port(self, value: int):
|
|
28
|
+
self._mqtt_port = int(value)
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
def client_id(self, value: str):
|
|
32
|
+
self._mqtt_client_id = value or ""
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def device_id(self, value: str):
|
|
36
|
+
self._device_id = value or ""
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
def mqtt_username(self, value: str):
|
|
40
|
+
self._mqtt_username = value or ""
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
def mqtt_password(self, value: str):
|
|
44
|
+
self._mqtt_password = value or ""
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def get_ssid(self) -> str:
|
|
49
|
+
return self._ssid
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def get_password(self) -> str:
|
|
53
|
+
return self._password
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def get_mqtt_server(self) -> str:
|
|
57
|
+
return self._mqtt_server
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def get_mqtt_port(self) -> int:
|
|
61
|
+
return self._mqtt_port
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def get_mqtt_client_id(self) -> str:
|
|
65
|
+
return self._mqtt_client_id
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def get_device_id(self) -> str:
|
|
69
|
+
return self._device_id
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def get_mqtt_username(self) -> str:
|
|
73
|
+
return self._mqtt_username
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def get_mqtt_password(self) -> str:
|
|
77
|
+
return self._mqtt_password
|
|
78
|
+
|
|
79
|
+
def get_data_topic(self) -> str:
|
|
80
|
+
return f"devices/{self._device_id}/data"
|
|
81
|
+
|
|
82
|
+
def get_status_topic(self) -> str:
|
|
83
|
+
return f"devices/{self._device_id}/status"
|
|
84
|
+
|
|
85
|
+
def get_commands_topic(self) -> str:
|
|
86
|
+
return f"devices/{self._device_id}/commands"
|
|
87
|
+
|
|
88
|
+
def get_command_response_topic(self) -> str:
|
|
89
|
+
return f"devices/{self._device_id}/command-response"
|
|
90
|
+
|
|
91
|
+
def is_valid(self) -> bool:
|
|
92
|
+
return bool(
|
|
93
|
+
self._mqtt_server
|
|
94
|
+
and self._mqtt_client_id
|
|
95
|
+
and self._device_id
|
|
96
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Control capability (mirrors Arduino Receive)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
8
|
+
|
|
9
|
+
from .ref import Ref
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .device_config import DeviceConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Receive:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
name: str,
|
|
19
|
+
type_: str,
|
|
20
|
+
pin: str,
|
|
21
|
+
entry_point: Ref,
|
|
22
|
+
client: Any,
|
|
23
|
+
config: DeviceConfig,
|
|
24
|
+
min_value: Optional[float] = None,
|
|
25
|
+
max_value: Optional[float] = None,
|
|
26
|
+
):
|
|
27
|
+
self._client = client
|
|
28
|
+
self._config = config
|
|
29
|
+
self.name = name
|
|
30
|
+
self.type = type_
|
|
31
|
+
self.pin = pin
|
|
32
|
+
self.entry_point = entry_point
|
|
33
|
+
self.has_range = min_value is not None and max_value is not None
|
|
34
|
+
self.min = float(min_value) if min_value is not None else 0.0
|
|
35
|
+
self.max = float(max_value) if max_value is not None else 0.0
|
|
36
|
+
self.is_registered = False
|
|
37
|
+
self.default_delay = 0.05
|
|
38
|
+
|
|
39
|
+
def get_name(self) -> str:
|
|
40
|
+
return self.name
|
|
41
|
+
|
|
42
|
+
def get_type(self) -> str:
|
|
43
|
+
return self.type
|
|
44
|
+
|
|
45
|
+
def get_pin(self) -> str:
|
|
46
|
+
return self.pin
|
|
47
|
+
|
|
48
|
+
def register_capability(self) -> bool:
|
|
49
|
+
control = {"type": self.type, "pin": self.pin}
|
|
50
|
+
if self.type in ("float", "integer") and self.has_range:
|
|
51
|
+
control["min"] = self.min
|
|
52
|
+
control["max"] = self.max
|
|
53
|
+
|
|
54
|
+
payload = {
|
|
55
|
+
"deviceId": self._config.get_device_id,
|
|
56
|
+
"type": "capabilities_control",
|
|
57
|
+
"controlName": self.name,
|
|
58
|
+
"control": control,
|
|
59
|
+
}
|
|
60
|
+
topic = f"devices/{self._config.get_device_id}/capabilities_control/{self.name}"
|
|
61
|
+
published = self._publish(topic, payload, retain=True)
|
|
62
|
+
print(f"{self.name} control published: {'SUCCESS' if published else 'FAILED'}")
|
|
63
|
+
if published:
|
|
64
|
+
self.is_registered = True
|
|
65
|
+
time.sleep(self.default_delay)
|
|
66
|
+
return published
|
|
67
|
+
|
|
68
|
+
def handle_command(self, value: Any, result: dict) -> bool:
|
|
69
|
+
if self.type == "float":
|
|
70
|
+
float_value = float(value)
|
|
71
|
+
if self.has_range:
|
|
72
|
+
float_value = max(self.min, min(self.max, float_value))
|
|
73
|
+
self.entry_point.value = float_value
|
|
74
|
+
result["newState"] = float_value
|
|
75
|
+
print(
|
|
76
|
+
f"[RX] pin={self.pin} name={self.name} value={float_value} "
|
|
77
|
+
f"<- backend"
|
|
78
|
+
)
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
if self.type == "integer":
|
|
82
|
+
int_value = int(value)
|
|
83
|
+
if self.has_range:
|
|
84
|
+
int_value = int(max(self.min, min(self.max, int_value)))
|
|
85
|
+
self.entry_point.value = int_value
|
|
86
|
+
result["newState"] = int_value
|
|
87
|
+
print(
|
|
88
|
+
f"[RX] pin={self.pin} name={self.name} value={int_value} "
|
|
89
|
+
f"<- backend"
|
|
90
|
+
)
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
if self.type == "boolean":
|
|
94
|
+
str_value = str(value).strip().lower()
|
|
95
|
+
bool_value = 1.0 if str_value in ("on", "true", "1") else 0.0
|
|
96
|
+
self.entry_point.value = bool_value
|
|
97
|
+
result["newState"] = str(value)
|
|
98
|
+
print(
|
|
99
|
+
f"[RX] pin={self.pin} name={self.name} value={str_value} "
|
|
100
|
+
f"(truth={bool_value}) <- backend"
|
|
101
|
+
)
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
if self.type == "string":
|
|
105
|
+
str_value = str(value)
|
|
106
|
+
self.entry_point.value = str_value
|
|
107
|
+
result["newState"] = str_value
|
|
108
|
+
print(
|
|
109
|
+
f"[RX] pin={self.pin} name={self.name} value={str_value} "
|
|
110
|
+
f"<- backend"
|
|
111
|
+
)
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
def _publish(self, topic: str, payload: dict, retain: bool) -> bool:
|
|
117
|
+
if self._client is None or not self._client.is_connected():
|
|
118
|
+
return False
|
|
119
|
+
try:
|
|
120
|
+
info = self._client.publish(topic, json.dumps(payload), retain=retain)
|
|
121
|
+
return info.rc == 0
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
print(f"Receive publish error: {exc}")
|
|
124
|
+
return False
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Mutable value holder (Arduino float* / String* equivalent)."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Ref:
|
|
5
|
+
"""Hold a value that write()/read() can update in place.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
sensor = Ref(0.0)
|
|
10
|
+
IOTFIY.write("SensorValue", "float", "A0", sensor)
|
|
11
|
+
sensor.value = 42.5
|
|
12
|
+
IOTFIY.launch()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__slots__ = ("value",)
|
|
16
|
+
|
|
17
|
+
def __init__(self, value=0.0):
|
|
18
|
+
self.value = value
|
|
19
|
+
|
|
20
|
+
def __repr__(self):
|
|
21
|
+
return f"Ref({self.value!r})"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Output / sensor capability (mirrors Arduino Send)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
8
|
+
|
|
9
|
+
from .ref import Ref
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .device_config import DeviceConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Send:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
name: str,
|
|
19
|
+
type_: str,
|
|
20
|
+
pin: str,
|
|
21
|
+
exit_point: Ref,
|
|
22
|
+
client: Any,
|
|
23
|
+
config: DeviceConfig,
|
|
24
|
+
min_value: Optional[float] = None,
|
|
25
|
+
max_value: Optional[float] = None,
|
|
26
|
+
):
|
|
27
|
+
self._client = client
|
|
28
|
+
self._config = config
|
|
29
|
+
self.name = name
|
|
30
|
+
self.type = type_
|
|
31
|
+
self.pin = pin
|
|
32
|
+
self.exit_point = exit_point
|
|
33
|
+
self.has_range = min_value is not None and max_value is not None
|
|
34
|
+
self.min = float(min_value) if min_value is not None else 0.0
|
|
35
|
+
self.max = float(max_value) if max_value is not None else 0.0
|
|
36
|
+
self.is_registered = False
|
|
37
|
+
self.default_delay = 0.05
|
|
38
|
+
|
|
39
|
+
def get_name(self) -> str:
|
|
40
|
+
return self.name
|
|
41
|
+
|
|
42
|
+
def get_type(self) -> str:
|
|
43
|
+
return self.type
|
|
44
|
+
|
|
45
|
+
def get_pin(self) -> str:
|
|
46
|
+
return self.pin
|
|
47
|
+
|
|
48
|
+
def get_current_value(self) -> float:
|
|
49
|
+
return float(self.exit_point.value)
|
|
50
|
+
|
|
51
|
+
def set_delay(self, ms: int) -> None:
|
|
52
|
+
self.default_delay = max(0, ms) / 1000.0
|
|
53
|
+
|
|
54
|
+
def register_capability(self) -> bool:
|
|
55
|
+
payload = {
|
|
56
|
+
"deviceId": self._config.get_device_id,
|
|
57
|
+
"type": "capabilities_output",
|
|
58
|
+
"outputName": self.name,
|
|
59
|
+
"output": {"type": self.type, "pin": self.pin},
|
|
60
|
+
}
|
|
61
|
+
topic = f"devices/{self._config.get_device_id}/capabilities_output/{self.name}"
|
|
62
|
+
published = self._publish(topic, payload, retain=True)
|
|
63
|
+
print(f"{self.name} output published: {'SUCCESS' if published else 'FAILED'}")
|
|
64
|
+
if published:
|
|
65
|
+
self.is_registered = True
|
|
66
|
+
time.sleep(self.default_delay)
|
|
67
|
+
return published
|
|
68
|
+
|
|
69
|
+
def publish_data(self) -> bool:
|
|
70
|
+
value = float(self.exit_point.value)
|
|
71
|
+
if self.has_range:
|
|
72
|
+
value = max(self.min, min(self.max, value))
|
|
73
|
+
|
|
74
|
+
if not self.is_registered:
|
|
75
|
+
self.register_capability()
|
|
76
|
+
|
|
77
|
+
data = {self.name: value}
|
|
78
|
+
if self.pin:
|
|
79
|
+
data[self.pin] = value
|
|
80
|
+
|
|
81
|
+
payload = {
|
|
82
|
+
"type": "output",
|
|
83
|
+
"deviceId": self._config.get_device_id,
|
|
84
|
+
"data": data,
|
|
85
|
+
}
|
|
86
|
+
published = self._publish(self._config.get_data_topic(), payload, retain=False)
|
|
87
|
+
status = "OK" if published else "FAIL"
|
|
88
|
+
print(
|
|
89
|
+
f"[TX] pin={self.pin} name={self.name} value={value} "
|
|
90
|
+
f"-> backend {status}"
|
|
91
|
+
)
|
|
92
|
+
return published
|
|
93
|
+
|
|
94
|
+
def _publish(self, topic: str, payload: dict, retain: bool) -> bool:
|
|
95
|
+
if self._client is None or not self._client.is_connected():
|
|
96
|
+
return False
|
|
97
|
+
try:
|
|
98
|
+
info = self._client.publish(topic, json.dumps(payload), retain=retain)
|
|
99
|
+
return info.rc == 0
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
print(f"Send publish error: {exc}")
|
|
102
|
+
return False
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iotfiy
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: IOTFIY MQTT gateway client for Raspberry Pi 3/4/5
|
|
5
|
+
Author-email: Iotfiy Solutions <iotfiy.solution@gmail.com>
|
|
6
|
+
Keywords: iot,mqtt,raspberry-pi,iotfiy
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: paho-mqtt>=1.6.1
|
|
9
|
+
Provides-Extra: gpio
|
|
10
|
+
Requires-Dist: gpiozero; extra == "gpio"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
src/iotfiy/__init__.py
|
|
3
|
+
src/iotfiy/client.py
|
|
4
|
+
src/iotfiy/device_config.py
|
|
5
|
+
src/iotfiy/receive.py
|
|
6
|
+
src/iotfiy/ref.py
|
|
7
|
+
src/iotfiy/send.py
|
|
8
|
+
src/iotfiy.egg-info/PKG-INFO
|
|
9
|
+
src/iotfiy.egg-info/SOURCES.txt
|
|
10
|
+
src/iotfiy.egg-info/dependency_links.txt
|
|
11
|
+
src/iotfiy.egg-info/requires.txt
|
|
12
|
+
src/iotfiy.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
iotfiy
|