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.
- foresttm23_iot-0.1.3.5/.gitignore +14 -0
- foresttm23_iot-0.1.3.5/Dockerfile +13 -0
- foresttm23_iot-0.1.3.5/PKG-INFO +159 -0
- foresttm23_iot-0.1.3.5/README.md +131 -0
- foresttm23_iot-0.1.3.5/diagrams/class.png +0 -0
- foresttm23_iot-0.1.3.5/diagrams/components.png +0 -0
- foresttm23_iot-0.1.3.5/diagrams/usecase.png +0 -0
- foresttm23_iot-0.1.3.5/docker-compose.yml +31 -0
- foresttm23_iot-0.1.3.5/iot_framework/__init__.py +44 -0
- foresttm23_iot-0.1.3.5/iot_framework/base/__init__.py +0 -0
- foresttm23_iot-0.1.3.5/iot_framework/base/base_device.py +43 -0
- foresttm23_iot-0.1.3.5/iot_framework/base/base_hub.py +237 -0
- foresttm23_iot-0.1.3.5/iot_framework/base/base_iot.py +77 -0
- foresttm23_iot-0.1.3.5/iot_framework/base/base_mqtt_client.py +39 -0
- foresttm23_iot-0.1.3.5/iot_framework/base/base_sensor.py +44 -0
- foresttm23_iot-0.1.3.5/iot_framework/config/__init__.py +0 -0
- foresttm23_iot-0.1.3.5/iot_framework/config/enums.py +11 -0
- foresttm23_iot-0.1.3.5/iot_framework/config/logger.py +14 -0
- foresttm23_iot-0.1.3.5/iot_framework/config/registry.py +15 -0
- foresttm23_iot-0.1.3.5/iot_framework/devices/__init__.py +2 -0
- foresttm23_iot-0.1.3.5/iot_framework/devices/lamp_device.py +40 -0
- foresttm23_iot-0.1.3.5/iot_framework/hubs/__init__.py +1 -0
- foresttm23_iot-0.1.3.5/iot_framework/hubs/hub.py +52 -0
- foresttm23_iot-0.1.3.5/iot_framework/sensors/__init__.py +2 -0
- foresttm23_iot-0.1.3.5/iot_framework/sensors/motion_sensor.py +38 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/__init__.py +0 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/integration/__init__.py +0 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/integration/conftest.py +84 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/integration/test_flow.py +68 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/tools/__init__.py +0 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/tools/admin.py +53 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/tools/observer.py +100 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/__init__.py +0 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/base/__init__.py +0 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/base/test_base_device.py +12 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/base/test_base_iot.py +75 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/base/test_base_sensor.py +12 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/conftest.py +109 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/devices/__init__.py +0 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/devices/test_lamp_device.py +69 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/hubs/__init__.py +0 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/hubs/test_hub.py +234 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/sensors/__init__.py +0 -0
- foresttm23_iot-0.1.3.5/iot_framework/tests/unittests/sensors/test_motion_sensor.py +35 -0
- foresttm23_iot-0.1.3.5/iot_framework/utils/__init__.py +0 -0
- foresttm23_iot-0.1.3.5/iot_framework/utils/utils.py +60 -0
- foresttm23_iot-0.1.3.5/load_test.jmx +225 -0
- foresttm23_iot-0.1.3.5/main.py +55 -0
- foresttm23_iot-0.1.3.5/mosquitto.conf +2 -0
- foresttm23_iot-0.1.3.5/pyproject.toml +45 -0
- foresttm23_iot-0.1.3.5/uv.lock +777 -0
|
@@ -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.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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)
|