foresttm23-iot 0.1.3.5__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.
Files changed (51) hide show
  1. foresttm23_iot-0.1.3.5/.gitignore +14 -0
  2. foresttm23_iot-0.1.3.5/Dockerfile +13 -0
  3. foresttm23_iot-0.1.3.5/PKG-INFO +159 -0
  4. foresttm23_iot-0.1.3.5/README.md +131 -0
  5. foresttm23_iot-0.1.3.5/diagrams/class.png +0 -0
  6. foresttm23_iot-0.1.3.5/diagrams/components.png +0 -0
  7. foresttm23_iot-0.1.3.5/diagrams/usecase.png +0 -0
  8. foresttm23_iot-0.1.3.5/docker-compose.yml +31 -0
  9. foresttm23_iot-0.1.3.5/iot_framework/__init__.py +44 -0
  10. foresttm23_iot-0.1.3.5/iot_framework/base/__init__.py +0 -0
  11. foresttm23_iot-0.1.3.5/iot_framework/base/base_device.py +43 -0
  12. foresttm23_iot-0.1.3.5/iot_framework/base/base_hub.py +237 -0
  13. foresttm23_iot-0.1.3.5/iot_framework/base/base_iot.py +77 -0
  14. foresttm23_iot-0.1.3.5/iot_framework/base/base_mqtt_client.py +39 -0
  15. foresttm23_iot-0.1.3.5/iot_framework/base/base_sensor.py +44 -0
  16. foresttm23_iot-0.1.3.5/iot_framework/config/__init__.py +0 -0
  17. foresttm23_iot-0.1.3.5/iot_framework/config/enums.py +11 -0
  18. foresttm23_iot-0.1.3.5/iot_framework/config/logger.py +14 -0
  19. foresttm23_iot-0.1.3.5/iot_framework/config/registry.py +15 -0
  20. foresttm23_iot-0.1.3.5/iot_framework/devices/__init__.py +2 -0
  21. foresttm23_iot-0.1.3.5/iot_framework/devices/lamp_device.py +40 -0
  22. foresttm23_iot-0.1.3.5/iot_framework/hubs/__init__.py +1 -0
  23. foresttm23_iot-0.1.3.5/iot_framework/hubs/hub.py +52 -0
  24. foresttm23_iot-0.1.3.5/iot_framework/sensors/__init__.py +2 -0
  25. foresttm23_iot-0.1.3.5/iot_framework/sensors/motion_sensor.py +38 -0
  26. foresttm23_iot-0.1.3.5/iot_framework/tests/__init__.py +0 -0
  27. foresttm23_iot-0.1.3.5/iot_framework/tests/integration/__init__.py +0 -0
  28. foresttm23_iot-0.1.3.5/iot_framework/tests/integration/conftest.py +84 -0
  29. foresttm23_iot-0.1.3.5/iot_framework/tests/integration/test_flow.py +68 -0
  30. foresttm23_iot-0.1.3.5/iot_framework/tests/tools/__init__.py +0 -0
  31. foresttm23_iot-0.1.3.5/iot_framework/tests/tools/admin.py +53 -0
  32. foresttm23_iot-0.1.3.5/iot_framework/tests/tools/observer.py +100 -0
  33. foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/__init__.py +0 -0
  34. foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/base/__init__.py +0 -0
  35. foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/base/test_base_device.py +12 -0
  36. foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/base/test_base_iot.py +75 -0
  37. foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/base/test_base_sensor.py +12 -0
  38. foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/conftest.py +109 -0
  39. foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/devices/__init__.py +0 -0
  40. foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/devices/test_lamp_device.py +69 -0
  41. foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/hubs/__init__.py +0 -0
  42. foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/hubs/test_hub.py +234 -0
  43. foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/sensors/__init__.py +0 -0
  44. foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/sensors/test_motion_sensor.py +35 -0
  45. foresttm23_iot-0.1.3.5/iot_framework/utils/__init__.py +0 -0
  46. foresttm23_iot-0.1.3.5/iot_framework/utils/utils.py +60 -0
  47. foresttm23_iot-0.1.3.5/load_test.jmx +225 -0
  48. foresttm23_iot-0.1.3.5/main.py +55 -0
  49. foresttm23_iot-0.1.3.5/mosquitto.conf +2 -0
  50. foresttm23_iot-0.1.3.5/pyproject.toml +45 -0
  51. foresttm23_iot-0.1.3.5/uv.lock +777 -0
@@ -0,0 +1,14 @@
1
+ .venv
2
+ .idea
3
+ .env
4
+
5
+ __pycache__/
6
+ *.pyc
7
+
8
+ *.coverage
9
+
10
+ *logs
11
+
12
+ *.pypirc
13
+
14
+ dist
@@ -0,0 +1,13 @@
1
+ FROM python:3.13-slim
2
+
3
+ WORKDIR /iot_framework
4
+
5
+ RUN pip install uv
6
+
7
+ COPY pyproject.toml .
8
+
9
+ RUN uv pip install --system --no-cache -r pyproject.toml
10
+
11
+ COPY . .
12
+
13
+ CMD ["python", "main.py"]
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.5
2
+ Name: foresttm23-iot
3
+ Version: 0.1.3.5
4
+ Summary: Implementation of the IoT system. For usage you have to run a mqtt broker.
5
+ Project-URL: Homepage, https://github.com/Foresttm23/mqtt-iot-framework
6
+ Project-URL: Repository, https://github.com/Foresttm23/mqtt-iot-framework
7
+ Project-URL: Issues, https://github.com/Foresttm23/mqtt-iot-framework/issues
8
+ Author-email: Foresttm23 <max.gogulia@gmail.com>
9
+ Keywords: devices,home-automation,iot,mqtt,sensors,smart-home
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Home Automation
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.13
17
+ Requires-Dist: loguru>=0.7.3
18
+ Requires-Dist: paho-mqtt>=2.1.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: build>=1.3.0; extra == 'dev'
21
+ Requires-Dist: hatchling>=1.28.0; extra == 'dev'
22
+ Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
23
+ Requires-Dist: pytest-mock>=3.15.1; extra == 'dev'
24
+ Requires-Dist: pytest>=9.0.2; extra == 'dev'
25
+ Requires-Dist: testcontainers[mqtt]>=4.13.3; extra == 'dev'
26
+ Requires-Dist: twine>=7.0.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Extensible MQTT IoT Communication Framework
30
+
31
+ A lightweight **Python framework** for building **MQTT-based IoT systems**. It provides reusable base classes for hubs, devices, sensors, and MQTT clients, making it easier to implement custom IoT workflows with a consistent architecture.
32
+
33
+ - **PyPI:** [foresttm23-iot](https://pypi.org/project/foresttm23-iot/)
34
+ - **Requires:** Python 3.13+
35
+
36
+ ## Overview
37
+
38
+ This framework is designed for object-oriented IoT development and is centered around reusable base abstractions for:
39
+
40
+ - `BaseMqttClient` — base class for MQTT clients
41
+ - `BaseIot` — shared base class for sensors and devices
42
+ - `BaseDevice` — base class for IoT devices
43
+ - `BaseSensor` — base class for IoT sensors
44
+ - `BaseHub` — base class for IoT hubs
45
+
46
+ It also includes a registry system that allows IoT classes to be dynamically discovered and instantiated by their `sub_type`.
47
+
48
+ ## Key Features
49
+
50
+ - MQTT-based IoT architecture
51
+ - Reusable base classes for hubs, devices, and sensors
52
+ - Class registry for dynamic IoT type lookup
53
+ - Support for custom and extended IoT implementations
54
+ - Built-in logging and helper utilities
55
+ - Designed to work with an MQTT broker
56
+
57
+ ## Core Concepts
58
+
59
+ ### Registry System
60
+
61
+ Use the `@register_iot` decorator to register your IoT classes:
62
+
63
+ ```python
64
+ from iot_framework import BaseDevice, register_iot
65
+
66
+
67
+ @register_iot
68
+ class LampDevice(BaseDevice):
69
+ sub_type = "lamp"
70
+ ```
71
+
72
+ This registers the class in the framework’s internal registry so it can be instantiated dynamically with `get_iot_class()`.
73
+
74
+ ### Base Classes
75
+
76
+ - **`BaseMqttClient`**: Provides the shared MQTT client behavior.
77
+ - **`BaseIot`**: Adds shared IoT functionality for sensors and devices.
78
+ - **`BaseDevice` / `BaseSensor`**: Specialized implementations for device and sensor behavior.
79
+ - **`BaseHub`**: Coordinates devices and sensors, handles discovery, and manages device interactions.
80
+
81
+ ## Installation
82
+
83
+ Install the package from PyPI:
84
+
85
+ ```bash
86
+ pip install foresttm23-iot
87
+ ```
88
+
89
+ Or install dependencies for local development using the project’s environment setup.
90
+
91
+ ## Usage
92
+
93
+ ### Run the framework entry point
94
+
95
+ ```bash
96
+ python main.py <hub_sub_type> <device_sub_type> <sensor_sub_type>
97
+ ```
98
+
99
+ Example:
100
+
101
+ ```bash
102
+ python main.py hub lamp motion
103
+ ```
104
+
105
+ This launches the requested IoT processes and connects them to the configured MQTT broker.
106
+
107
+ ### Example Device
108
+
109
+ ```python
110
+ import json
111
+ import time
112
+
113
+ from iot_framework import BaseDevice, logger, register_iot
114
+
115
+
116
+ @register_iot
117
+ class LampDevice(BaseDevice):
118
+ sub_type = "lamp"
119
+
120
+ def __init__(self, client, device_id: str):
121
+ super().__init__(client, device_id, self.sub_type)
122
+
123
+ def on_message(self, client, userdata, msg) -> None:
124
+ payload = json.loads(msg.payload.decode())
125
+ command = payload.get("command").lower()
126
+ transaction_id = payload.get("transaction_id")
127
+
128
+ logger.info(f"📩 [{self.id}] Received command: {command}")
129
+ self.set_state(command)
130
+
131
+ lamp_payload = json.dumps(
132
+ {
133
+ "state": self.state,
134
+ "transaction_id": transaction_id,
135
+ "timestamp": time.time(),
136
+ }
137
+ )
138
+ self.client.publish(self.state_topic, lamp_payload, retain=True)
139
+ ```
140
+
141
+ ## Requirements
142
+
143
+ - Python 3.13+
144
+ - MQTT broker
145
+ - `loguru`
146
+ - `paho-mqtt`
147
+
148
+ ## Project Structure
149
+
150
+ - `iot_framework/` — framework package
151
+ - `main.py` — launcher for IoT processes
152
+ - `docker-compose.yml` — container setup
153
+ - `Dockerfile` — image definition
154
+ - `load_test.jmx` — performance/load testing
155
+ - `mosquitto.conf` — MQTT broker configuration
156
+
157
+ ## License
158
+
159
+ No license has been specified for this project.
@@ -0,0 +1,131 @@
1
+ # Extensible MQTT IoT Communication Framework
2
+
3
+ A lightweight **Python framework** for building **MQTT-based IoT systems**. It provides reusable base classes for hubs, devices, sensors, and MQTT clients, making it easier to implement custom IoT workflows with a consistent architecture.
4
+
5
+ - **PyPI:** [foresttm23-iot](https://pypi.org/project/foresttm23-iot/)
6
+ - **Requires:** Python 3.13+
7
+
8
+ ## Overview
9
+
10
+ This framework is designed for object-oriented IoT development and is centered around reusable base abstractions for:
11
+
12
+ - `BaseMqttClient` — base class for MQTT clients
13
+ - `BaseIot` — shared base class for sensors and devices
14
+ - `BaseDevice` — base class for IoT devices
15
+ - `BaseSensor` — base class for IoT sensors
16
+ - `BaseHub` — base class for IoT hubs
17
+
18
+ It also includes a registry system that allows IoT classes to be dynamically discovered and instantiated by their `sub_type`.
19
+
20
+ ## Key Features
21
+
22
+ - MQTT-based IoT architecture
23
+ - Reusable base classes for hubs, devices, and sensors
24
+ - Class registry for dynamic IoT type lookup
25
+ - Support for custom and extended IoT implementations
26
+ - Built-in logging and helper utilities
27
+ - Designed to work with an MQTT broker
28
+
29
+ ## Core Concepts
30
+
31
+ ### Registry System
32
+
33
+ Use the `@register_iot` decorator to register your IoT classes:
34
+
35
+ ```python
36
+ from iot_framework import BaseDevice, register_iot
37
+
38
+
39
+ @register_iot
40
+ class LampDevice(BaseDevice):
41
+ sub_type = "lamp"
42
+ ```
43
+
44
+ This registers the class in the framework’s internal registry so it can be instantiated dynamically with `get_iot_class()`.
45
+
46
+ ### Base Classes
47
+
48
+ - **`BaseMqttClient`**: Provides the shared MQTT client behavior.
49
+ - **`BaseIot`**: Adds shared IoT functionality for sensors and devices.
50
+ - **`BaseDevice` / `BaseSensor`**: Specialized implementations for device and sensor behavior.
51
+ - **`BaseHub`**: Coordinates devices and sensors, handles discovery, and manages device interactions.
52
+
53
+ ## Installation
54
+
55
+ Install the package from PyPI:
56
+
57
+ ```bash
58
+ pip install foresttm23-iot
59
+ ```
60
+
61
+ Or install dependencies for local development using the project’s environment setup.
62
+
63
+ ## Usage
64
+
65
+ ### Run the framework entry point
66
+
67
+ ```bash
68
+ python main.py <hub_sub_type> <device_sub_type> <sensor_sub_type>
69
+ ```
70
+
71
+ Example:
72
+
73
+ ```bash
74
+ python main.py hub lamp motion
75
+ ```
76
+
77
+ This launches the requested IoT processes and connects them to the configured MQTT broker.
78
+
79
+ ### Example Device
80
+
81
+ ```python
82
+ import json
83
+ import time
84
+
85
+ from iot_framework import BaseDevice, logger, register_iot
86
+
87
+
88
+ @register_iot
89
+ class LampDevice(BaseDevice):
90
+ sub_type = "lamp"
91
+
92
+ def __init__(self, client, device_id: str):
93
+ super().__init__(client, device_id, self.sub_type)
94
+
95
+ def on_message(self, client, userdata, msg) -> None:
96
+ payload = json.loads(msg.payload.decode())
97
+ command = payload.get("command").lower()
98
+ transaction_id = payload.get("transaction_id")
99
+
100
+ logger.info(f"📩 [{self.id}] Received command: {command}")
101
+ self.set_state(command)
102
+
103
+ lamp_payload = json.dumps(
104
+ {
105
+ "state": self.state,
106
+ "transaction_id": transaction_id,
107
+ "timestamp": time.time(),
108
+ }
109
+ )
110
+ self.client.publish(self.state_topic, lamp_payload, retain=True)
111
+ ```
112
+
113
+ ## Requirements
114
+
115
+ - Python 3.13+
116
+ - MQTT broker
117
+ - `loguru`
118
+ - `paho-mqtt`
119
+
120
+ ## Project Structure
121
+
122
+ - `iot_framework/` — framework package
123
+ - `main.py` — launcher for IoT processes
124
+ - `docker-compose.yml` — container setup
125
+ - `Dockerfile` — image definition
126
+ - `load_test.jmx` — performance/load testing
127
+ - `mosquitto.conf` — MQTT broker configuration
128
+
129
+ ## License
130
+
131
+ No license has been specified for this project.
@@ -0,0 +1,31 @@
1
+ services:
2
+ mqtt:
3
+ image: eclipse-mosquitto:2
4
+ container_name: mqtt-broker
5
+ ports:
6
+ - "1883:1883"
7
+ volumes:
8
+ - ./mosquitto.conf:/mosquitto/config/mosquitto.conf
9
+
10
+ smart-hub:
11
+ build: .
12
+ command: python main.py hub # ЗаĐŋ҃ҁĐēĐ°Ņ”ĐŧĐž ҂ҖĐģҌĐēи ĐĨай
13
+ depends_on:
14
+ - mqtt
15
+ environment:
16
+ - MQTT_BROKER=mqtt
17
+ - MQTT_PORT=1883
18
+ volumes:
19
+ - .:/iot_framework
20
+
21
+ devices-sim:
22
+ build: .
23
+ command: python main.py lamp lamp motion motion
24
+ depends_on:
25
+ - mqtt
26
+ - smart-hub
27
+ environment:
28
+ - MQTT_BROKER=mqtt
29
+ - MQTT_PORT=1883
30
+ volumes:
31
+ - .:/iot_framework
@@ -0,0 +1,44 @@
1
+ """
2
+ IoT Framework - A library for building MQTT-based IoT systems.
3
+ """
4
+
5
+ from importlib.metadata import version
6
+
7
+ # Base
8
+ from iot_framework.base.base_device import BaseDevice
9
+ from iot_framework.base.base_hub import BaseHub
10
+ from iot_framework.base.base_iot import BaseIot
11
+ from iot_framework.base.base_mqtt_client import BaseMqttClient
12
+ from iot_framework.base.base_sensor import BaseSensor
13
+ # Enums
14
+ from iot_framework.config.enums import STATE, STATUS
15
+ # Logger
16
+ from iot_framework.config.logger import logger
17
+ # Registry
18
+ from iot_framework.config.registry import register_iot, DEVICE_CLASSES, SENSOR_CLASSES, HUB_CLASSES
19
+ # Utils
20
+ from iot_framework.utils.utils import get_iot_class, start_process
21
+
22
+ __version__ = version("foresttm23-iot")
23
+
24
+ __all__ = [
25
+ # Base classes
26
+ "BaseMqttClient",
27
+ "BaseIot",
28
+ "BaseDevice",
29
+ "BaseSensor",
30
+ "BaseHub",
31
+ # Enums
32
+ "STATE",
33
+ "STATUS",
34
+ # Logger
35
+ "logger",
36
+ # Registry
37
+ "register_iot",
38
+ "DEVICE_CLASSES",
39
+ "SENSOR_CLASSES",
40
+ "HUB_CLASSES",
41
+ # Utils
42
+ "get_iot_class",
43
+ "start_process"
44
+ ]
File without changes
@@ -0,0 +1,43 @@
1
+ import json
2
+ from abc import abstractmethod
3
+
4
+ import paho.mqtt.client as mqtt
5
+
6
+ from iot_framework.base.base_iot import BaseIot
7
+
8
+
9
+ class BaseDevice(BaseIot):
10
+ """
11
+ Base class for all devices.
12
+
13
+ See Also:
14
+ - LampDevice in iot_framework.devices.lamp_device for a full implementation
15
+ """
16
+
17
+ iot_type = "device"
18
+
19
+ def __init__(self, client: mqtt.Client, device_id: str, sub_type: str):
20
+ prefix = "devices"
21
+ super().__init__(client=client, iot_id=device_id, sub_type=sub_type, prefix=prefix)
22
+ self.state_topic = f"{self.base_topic}/state"
23
+
24
+ def get_birth_payload(self) -> str:
25
+ device_info = {
26
+ "id": self.id,
27
+ "iot_type": self.iot_type,
28
+ "sub_type": self.sub_type,
29
+ "state_topic": self.state_topic,
30
+ "status_topic": self.status_topic,
31
+ "control_topic": self.control_topic,
32
+
33
+ "status": self.status
34
+ }
35
+ return json.dumps(device_info)
36
+
37
+ @abstractmethod
38
+ def on_message(self, client, userdata, msg) -> None: # pragma: no cover
39
+ pass
40
+
41
+ @abstractmethod
42
+ def run(self): # pragma: no cover
43
+ pass
@@ -0,0 +1,237 @@
1
+ import json
2
+ import threading
3
+ import time
4
+ from uuid import uuid4
5
+
6
+ import paho.mqtt.client as mqtt
7
+
8
+ from iot_framework.base.base_mqtt_client import BaseMqttClient
9
+ from iot_framework.config.enums import STATUS
10
+ from iot_framework.config.logger import logger
11
+
12
+
13
+ class BaseHub(BaseMqttClient):
14
+ """
15
+ Base class for all hubs.
16
+
17
+ See Also:
18
+ - SmartHub in iot_framework.hubs.hub for a full implementation
19
+ """
20
+
21
+ iot_type = "hub"
22
+
23
+ def __init__(self, client: mqtt.Client, hub_id: str):
24
+ super().__init__(client, hub_id)
25
+ self.devices = {}
26
+ self.sensors = {}
27
+
28
+ # Saves transaction_id: {"iot_id": id, "timestamp", time.time(), "expected_state": new_state}
29
+ # We can later add handling for the time. So if time is elapsed, we delete the transaction or stop the device.
30
+ self.pending_transactions = {}
31
+
32
+ self.device_timers = {}
33
+
34
+ self.status_topic = f"hub/{self.id}/status"
35
+
36
+ def on_connect(self, client, userdata, flags, rc, properties) -> None:
37
+ if rc == 0:
38
+ logger.info(f"✅ [HUB] Connected with ID {self.id}")
39
+
40
+ self.status = STATUS.ONLINE.value
41
+ client.publish(self.status_topic, json.dumps({"status": self.status}), retain=True)
42
+
43
+ client.subscribe("discovery/+")
44
+ client.subscribe("sensors/+/+/data")
45
+ client.subscribe("devices/+/+/state")
46
+ client.subscribe("+/+/+/status")
47
+ else:
48
+ logger.warning(f"❌ [HUB] Connection failed: {rc}")
49
+
50
+ def on_message(self, client, userdata, msg) -> None:
51
+ """Handles basic functionality for discovery, state and status messages."""
52
+ try:
53
+ if "discovery/" in msg.topic:
54
+ self.handle_discovery(msg)
55
+
56
+ # State can only be in devices
57
+ if "/state" in msg.topic:
58
+ self.handle_device_state(msg)
59
+
60
+ # Status is used for both
61
+ if "/status" in msg.topic:
62
+ self.handle_status(msg)
63
+
64
+ except Exception as e:
65
+ logger.info(f"âš ī¸ [HUB] Error: {e}")
66
+
67
+ @staticmethod
68
+ def _get_payload(msg):
69
+ try:
70
+ payload = json.loads(msg.payload.decode())
71
+ return payload
72
+ except json.JSONDecodeError:
73
+ logger.warning(f"❌ Failed to decode JSON from {msg.topic}")
74
+ return None
75
+
76
+ @staticmethod
77
+ def _get_field_from_payload(payload, field_name: str, default_value=None):
78
+ field_value = payload.get(field_name, default_value)
79
+ if field_value is None:
80
+ raise ValueError(f"Missing required field: {field_name}")
81
+
82
+ return field_value
83
+
84
+ def _register_iot(self, iot_id: str, iot_type: str, payload: dict, base_iot_device: dict) -> None:
85
+ if iot_type == "device":
86
+ state_topic = self._get_field_from_payload(payload, "state_topic")
87
+ self.devices[iot_id] = {"state_topic": state_topic, **base_iot_device}
88
+ logger.info(f"📝 [HUB] Registered device: {self.devices[iot_id]}")
89
+
90
+ elif iot_type == "sensor":
91
+ data_topic = self._get_field_from_payload(payload, "data_topic")
92
+ self.sensors[iot_id] = {"data_topic": data_topic, **base_iot_device}
93
+ logger.info(f"📝 [HUB] Registered sensor: {self.sensors[iot_id]}")
94
+ else:
95
+ logger.info(f"âš ī¸ Unknown iot_type: {iot_type}")
96
+
97
+ def handle_discovery(self, msg):
98
+ payload = self._get_payload(msg)
99
+ if payload is None:
100
+ return
101
+
102
+ try:
103
+ iot_id = self._get_field_from_payload(payload, "id")
104
+ iot_type = self._get_field_from_payload(payload, "iot_type")
105
+ sub_type = self._get_field_from_payload(payload, "sub_type")
106
+ status_topic = self._get_field_from_payload(payload, "status_topic")
107
+ control_topic = self._get_field_from_payload(payload, "control_topic")
108
+
109
+ status = self._get_field_from_payload(payload, "status")
110
+ # We dont register offline devices.
111
+ if status == STATUS.OFFLINE.value:
112
+ return
113
+
114
+ base_iot_device = {"iot_type": iot_type,
115
+ "sub_type": sub_type,
116
+ "status_topic": status_topic,
117
+ "control_topic": control_topic,
118
+
119
+ "status": status}
120
+
121
+ self._register_iot(iot_id, iot_type, payload, base_iot_device)
122
+
123
+ except ValueError as e:
124
+ logger.warning(f"❌ Discovery Error for topic {msg.topic}: {e}")
125
+
126
+ def _publish_to_device(self, topic: str, command: str, device_id: str) -> None:
127
+ transaction_id = str(uuid4())
128
+ payload = json.dumps({"command": command, "transaction_id": transaction_id, "timestamp": time.time()})
129
+ self.client.publish(topic, payload)
130
+ self.pending_transactions[transaction_id] = {"iot_id": device_id, "timestamp": time.time(),
131
+ "expected_state": command}
132
+ logger.info(f"⚡ [HUB] Sent {command} to {device_id}")
133
+
134
+ def _publish_to_device_with_timer(self, topic: str, command: str, device_id: str, duration: int):
135
+ if device_id in self.device_timers:
136
+ logger.info(f"🔄 [HUB] Motion detected again! Resetting timer for {device_id}")
137
+ self.device_timers[device_id].cancel()
138
+
139
+ timer = threading.Timer(duration, self._publish_to_device, args=(topic, command, device_id))
140
+ self.device_timers[device_id] = timer
141
+ timer.start()
142
+
143
+ logger.info(f"🕒 [HUB] Scheduled Auto-OFF for {device_id} in {duration}s")
144
+
145
+ def handle_device_state(self, msg):
146
+ """
147
+ Handles state messages from devices.
148
+ For custom behavior overwrite _is_admin_transaction(), _validate_transaction(), _validate_timestamp() methods.
149
+ """
150
+ if msg.retain:
151
+ return
152
+
153
+ payload = self._get_payload(msg)
154
+ if payload is None:
155
+ return
156
+
157
+ new_state = self._get_field_from_payload(payload, "state")
158
+ timestamp = self._get_field_from_payload(payload, "timestamp")
159
+ transaction_id = self._get_field_from_payload(payload, "transaction_id")
160
+
161
+ if self._is_admin_transaction(transaction_id):
162
+ return
163
+ if not self._validate_transaction(transaction_id, new_state):
164
+ return
165
+ if not self._validate_timestamp(timestamp):
166
+ return
167
+
168
+ topic_parts = msg.topic.split("/")
169
+ device_id = topic_parts[2]
170
+
171
+ if device_id not in self.devices:
172
+ logger.warning(f"❓ [HUB] Received state from unknown iot device: {device_id}")
173
+ return
174
+
175
+ del self.pending_transactions[transaction_id]
176
+ logger.info(f"transaction_id: {transaction_id}")
177
+
178
+ @staticmethod
179
+ def _validate_timestamp(timestamp: float) -> bool:
180
+ """Validate timestamp is not too old."""
181
+ return time.time() - timestamp < 10
182
+
183
+ def _validate_transaction(self, transaction_id: str, new_state: str) -> bool:
184
+ """Validate transaction exists and state matches expected."""
185
+ if transaction_id not in self.pending_transactions:
186
+ logger.warning(f"❌ Unknown transaction_id: {transaction_id}")
187
+ return False
188
+
189
+ expected_state = self.pending_transactions[transaction_id]["expected_state"]
190
+ if expected_state != new_state:
191
+ logger.error(f"❌ State mismatch for {transaction_id}: expected {expected_state}, got {new_state}")
192
+ return False
193
+
194
+ return True
195
+
196
+ @staticmethod
197
+ def _is_admin_transaction(transaction_id: str) -> bool:
198
+ if transaction_id.startswith("admin-"):
199
+ logger.info(f"â„šī¸ [HUB] Acknowledging admin-initiated command: {transaction_id}")
200
+ return True
201
+ return False
202
+
203
+ def handle_status(self, msg):
204
+ """Updates internal status of a device or sensor from external status messages."""
205
+ payload = self._get_payload(msg)
206
+ if payload is None:
207
+ return
208
+
209
+ status = self._get_field_from_payload(payload, "status")
210
+
211
+ topic_parts = msg.topic.split("/")
212
+ iot_id = topic_parts[2]
213
+
214
+ self._update_iot_status(iot_id, status)
215
+
216
+ def _update_iot_status(self, iot_id: str, status: STATUS) -> None:
217
+ if iot_id in self.devices:
218
+ self.devices[iot_id]["status"] = status
219
+ logger.info(f"â„šī¸ [HUB] Iot device {iot_id} is now {status}")
220
+
221
+ elif iot_id in self.sensors:
222
+ self.sensors[iot_id]["status"] = status
223
+ logger.info(f"â„šī¸ [HUB] Iot device {iot_id} is now {status}")
224
+
225
+ def cancel_all_timers(self):
226
+ """
227
+ Cancel all timers for devices.
228
+ Can be used for tests.
229
+ """
230
+ for device_id, timer in self.device_timers.items():
231
+ timer.cancel()
232
+ self.device_timers.clear()
233
+
234
+ def run(self):
235
+ logger.info(f"🧠 [HUB] Running...")
236
+ while True:
237
+ time.sleep(10)