Skip to content

Commit 9164cba

Browse files
Initialize python library with core connectivity, auth, managers (#1198)
1 parent 8bbf5c2 commit 9164cba

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+4585
-7
lines changed

.gencode_hash.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ eadc72e31b4796273479967303513b16563af0f946d1e1c7eba1748f9b133d40 gencode/java/u
151151
d2a53a067185447ce672e5521cdb073a2b2100b9384b68e87211cafc5ef8cb2a gencode/presentation/presentation.json
152152
4cf98cbd132cde0cc8813ac35cf3712cb46014154c817c04ad2902c268cdd8fe gencode/python/pyproject.toml
153153
17bf15d2886841330ce894ba554bb6955722a1a2d17b8b89d569b633046e7b42 gencode/python/udmi/schema/__init__.py
154-
478b09b7e26c07441e86e1778f7e5ce8f8fa1b3fce88b8089064347d1e2fc67b gencode/python/udmi/schema/_base.py
154+
f9d90861e568b27445bef241f04cce64cc44731c95c8bd9e3f65cef79d42dab0 gencode/python/udmi/schema/_base.py
155155
0e18050ec17fde8162f75a76d9dc623d3f6ddca4396441bd603189827ed21a80 gencode/python/udmi/schema/access_iot.py
156156
32a951e2bf13f556082f8d94be079b3df6cc081b6ff59f71a82d32782cf8f8f6 gencode/python/udmi/schema/ancillary_properties.py
157157
b4f4a394ce4049fafe267a146458d5b1725e2532788c5811b4b0b96f84715e31 gencode/python/udmi/schema/building_config_entity.py

.github/workflows/testing.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,41 @@ jobs:
416416
run: |
417417
export SAVE_PATH=$PATH
418418
sudo -E bash -c 'PATH=$SAVE_PATH misc/discoverynode/testing/e2e/test_local site_model'
419+
udmilp:
420+
name: Python Library Tests
421+
runs-on: ubuntu-24.04
422+
timeout-minutes: 15
423+
strategy:
424+
matrix:
425+
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
426+
steps:
427+
- name: Checkout repository
428+
uses: actions/checkout@v4
429+
- name: Set up Python ${{ matrix.python-version }}
430+
uses: actions/setup-python@v5
431+
with:
432+
python-version: ${{ matrix.python-version }}
433+
- name: Install Poetry
434+
run: |
435+
pip install poetry
436+
poetry config virtualenvs.in-project true
437+
- name: Cache poetry venv
438+
id: cache-deps
439+
uses: actions/cache@v4
440+
with:
441+
path: .venv
442+
key: ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ hashFiles('poetry.lock') }}
443+
restore-keys: |
444+
${{ runner.os }}-${{ matrix.python-version }}-venv-
445+
- name: Install dependencies
446+
run: |
447+
poetry install --with dev
448+
- name: Run tests with pytest
449+
run: |
450+
cd clientlib/python
451+
poetry run pytest -v tests/
452+
- name: Run pylint
453+
if: matrix.python-version == '3.13'
454+
run: |
455+
cd clientlib/python
456+
poetry run pylint src tests

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ credentials.json
4343
/local/
4444
.pubber.pid
4545
__pycache__/
46+
.pytest_cache
4647
/tests/sites/*/out/
4748
/tests/sites/*/devices/*/out/
4849
/tests/sites/basic/extras/
@@ -57,3 +58,8 @@ cloud/gcp/udmi-sites.tf
5758
cloud/gcp/main.tf
5859
cloud/gcp/auth/credentials.json
5960
cloud/gcp/.terraform*
61+
62+
.python-version
63+
dist
64+
gencode/python/dist
65+
venv

clientlib/python/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# A Python Library for UDMI
2+
3+
This project is a high-level, extensible Python library designed to simplify the
4+
development of applications for the **Universal Device Management Interface (
5+
UDMI)**. It provides a clean, Pythonic abstraction over the core UDMI
6+
specification, lowering the barrier to entry for developers and device
7+
manufacturers seeking to create UDMI-compliant IoT solutions.
8+
9+
## Key Features
10+
11+
* **Strict Schema Adherence**: Ensures 1:1 compliance with the UDMI data model
12+
by using Python dataclasses that are auto-generated directly from the official
13+
UDMI JSON schemas.
14+
15+
* **Abstracted Complexity**: Handles core device functionalities like MQTT
16+
client communication, authentication (including certificate management), and
17+
the primary config/state message loop.
18+
19+
* **Extensible by Design**: Uses abstract base classes to allow for easy
20+
customization and support for future protocols or features.
21+
22+
The library acts as a reference implementation for demonstrating core
23+
functionalities like connectivity, telemetry, and message handling.
24+
25+
---
26+
27+
## Local Setup & Usage Examples
28+
29+
To install and build the library locally use the build script.
30+
31+
```shell
32+
${UDMI_ROOT}/clientlib/python/bin/build
33+
```
34+
35+
You can find a few samples demonstrating how to connect a device using different
36+
authentication methods as well as other features of the library in
37+
`$UDMI/ROOT/clientlib/python/samples`.

clientlib/python/bin/build

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/bin/bash -e
2+
#
3+
# This script builds the Python client library.
4+
#
5+
# It performs the following steps:
6+
# 1. Sets up path variables.
7+
# 2. Ensures Python 3 and pyproject.toml are present.
8+
# 3. Creates or updates a Python virtual environment (venv).
9+
# 4. Installs Poetry.
10+
# 5. Installs project dependencies via Poetry.
11+
# 6. Builds the final package (wheel and sdist).
12+
#
13+
14+
set -euo pipefail
15+
16+
# --- Variable Definitions ---
17+
echo "Setting up script variables..."
18+
SCRIPT_ROOT=$(realpath "$(dirname "$0")")
19+
LIB_ROOT=$(realpath "$SCRIPT_ROOT/..")
20+
UDMI_ROOT=$(realpath "$SCRIPT_ROOT/../../..")
21+
VENV_DIR="$LIB_ROOT/venv"
22+
PYPROJECT_FILE="$UDMI_ROOT/pyproject.toml"
23+
POETRY_VERSION="2.0.1"
24+
25+
# Source the common shell functions
26+
source "$UDMI_ROOT/etc/shell_common.sh"
27+
28+
# --- Prerequisite Checks ---
29+
echo "Checking prerequisites..."
30+
31+
# 1. Check for python3
32+
if ! command -v python3 &> /dev/null; then
33+
fail "python3 could not be found. Please install Python 3."
34+
fi
35+
echo " - python3 found."
36+
37+
# 2. Check for pyproject.toml
38+
if [ ! -f "$PYPROJECT_FILE" ]; then
39+
fail "pyproject.toml not found at $PYPROJECT_FILE. This script must be run from within a valid Poetry project."
40+
fi
41+
echo " - pyproject.toml found."
42+
43+
# --- Main Logic ---
44+
# 1. Setup python environment
45+
if [ -d "$VENV_DIR" ]; then
46+
echo "Virtual environment already exists. Activating."
47+
else
48+
echo "Creating new virtual environment at $VENV_DIR..."
49+
python3 -m venv "$VENV_DIR"
50+
fi
51+
source "$VENV_DIR/bin/activate"
52+
echo "Virtual environment activated."
53+
54+
# 2. Install/Upgrade Poetry
55+
echo "Installing Poetry v$POETRY_VERSION..."
56+
pip install "poetry==$POETRY_VERSION"
57+
echo "Poetry installed."
58+
59+
# 3. Install dependencies
60+
echo "Installing project dependencies with Poetry..."
61+
"$VENV_DIR/bin/poetry" install
62+
echo "Dependencies installed."
63+
64+
# 4. Build the package
65+
echo "Building the package..."
66+
"$VENV_DIR/bin/poetry" build
67+
echo "Package built successfully."
68+
69+
# --- Success ---
70+
echo "--------------------------------------------------------------"
71+
echo "Build complete. Artifacts are in $UDMI_ROOT/dist/"
72+
echo "--------------------------------------------------------------"
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
This sample demonstrates how to inject static device information (Make, Model,
3+
Firmware, etc.) into the UDMI State message using the standard SystemManager.
4+
5+
No custom manager classes are required for this common task.
6+
"""
7+
8+
import logging
9+
import sys
10+
11+
from udmi.core.factory import create_device_with_basic_auth
12+
from udmi.core.managers import SystemManager
13+
from udmi.schema import EndpointConfiguration
14+
15+
# --- Configuration ---
16+
DEVICE_ID = "AHU-1"
17+
MQTT_HOSTNAME = "localhost"
18+
MQTT_PORT = 1883
19+
BROKER_USERNAME = "pyudmi-device"
20+
BROKER_PASSWORD = "somesecureword"
21+
22+
logging.basicConfig(level=logging.INFO,
23+
format='%(asctime)s - %(name)s - %(message)s')
24+
LOGGER = logging.getLogger(__name__)
25+
26+
if __name__ == "__main__":
27+
try:
28+
# 1. Configure Connection
29+
topic_prefix = "/r/ZZ-TRI-FECTA/d/"
30+
endpoint = EndpointConfiguration(
31+
client_id=f"{topic_prefix}{DEVICE_ID}",
32+
hostname=MQTT_HOSTNAME,
33+
port=MQTT_PORT,
34+
topic_prefix=topic_prefix
35+
)
36+
37+
# 2. CONFIGURE SYSTEM MANAGER
38+
# We pre-configure the standard SystemManager with our device's details.
39+
# These will automatically appear in the 'system.hardware' and
40+
# 'system.software' sections of the published State message.
41+
my_system_manager = SystemManager(
42+
hardware_info={"make": "GenericDevice", "model": "SomeModel"},
43+
software_info={"firmware": "v2.4.5-stable", "os": "Linux"}
44+
)
45+
46+
# 3. Create Device
47+
# We pass our configured manager to the 'managers' argument, which
48+
# replaces the default empty SystemManager.
49+
device = create_device_with_basic_auth(
50+
endpoint_config=endpoint,
51+
username=BROKER_USERNAME,
52+
password=BROKER_PASSWORD,
53+
managers=[my_system_manager]
54+
)
55+
56+
device.run()
57+
58+
except KeyboardInterrupt:
59+
LOGGER.info("Stopped by user.")
60+
except Exception as e:
61+
LOGGER.critical(f"Fatal error: {e}", exc_info=True)
62+
sys.exit(1)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""
2+
Example script for connecting a UDMI device to Cloud IoT Core.
3+
4+
This script demonstrates how to use the `create_device_with_jwt` factory
5+
to instantiate a device, configure it with the necessary GCP IoT Core
6+
credentials (project, registry, device), and run its main loop.
7+
8+
The device will authenticate using a JWT generated from an RS256 private key.
9+
"""
10+
11+
import logging
12+
import sys
13+
14+
from udmi.core.factory import JwtAuthArgs
15+
from udmi.core.factory import create_device_with_jwt
16+
from udmi.schema import EndpointConfiguration
17+
18+
# --- Configuration Constants ---
19+
# CONFIGURE THESE VALUES to match your GCP IoT Core setup
20+
PROJECT_ID = "gcp-project-id"
21+
REGION = "us-central1"
22+
REGISTRY_ID = "ZZ-TRI-FECTA"
23+
DEVICE_ID = "AHU-1"
24+
MQTT_HOST = "mqtt.bos.goog"
25+
MQTT_PORT = 8883 # secure port, mqtt client will automatically use TLS
26+
ALGORITHM = "RS256" # Algorithm for signing the JWT
27+
PRIVATE_KEY_FILE = "/path/to/ZZ-TRI-FECTA/devices/AHU-1/rsa_private.pem"
28+
29+
# The full client ID string required by Cloud IoT Core's MQTT bridge
30+
CLIENT_ID = (
31+
f"projects/{PROJECT_ID}/locations/{REGION}/"
32+
f"registries/{REGISTRY_ID}/devices/{DEVICE_ID}"
33+
)
34+
35+
if __name__ == "__main__":
36+
# Set up basic logging to see device activity in the console
37+
logging.basicConfig(level=logging.DEBUG,
38+
format='%(asctime)s - %(levelname)s - %(message)s')
39+
40+
# Main execution block with error handling
41+
try:
42+
# 1. Create the UDMI EndpointConfiguration object
43+
# This tells the client where to connect
44+
endpoint_config = EndpointConfiguration(
45+
client_id=CLIENT_ID,
46+
hostname=MQTT_HOST,
47+
port=MQTT_PORT
48+
)
49+
logging.info("Creating device instance using the JWT factory...")
50+
51+
# 2. Use the factory to create the device instance.
52+
# This factory wires up all the necessary components:
53+
# - MqttMessagingClient: Handles the Paho MQTT connection
54+
# - JwtAuthProvider: Generates new JWTs when needed
55+
# - MessageDispatcher: Routes MQTT messages to/from the device
56+
# - Device: The main orchestrator
57+
# - SystemManager: The default manager for state/config
58+
device = create_device_with_jwt(
59+
endpoint_config=endpoint_config,
60+
jwt_auth_args=JwtAuthArgs(
61+
project_id=PROJECT_ID,
62+
key_file=PRIVATE_KEY_FILE,
63+
algorithm=ALGORITHM
64+
)
65+
)
66+
67+
# 3. Start the device's main loop.
68+
# This will:
69+
# a. Connect to the MQTT bridge
70+
# b. Handle the JWT authentication
71+
# c. Subscribe to config/command topics
72+
# d. Publish the initial state
73+
# e. Enter the continuous run loop
74+
device.run()
75+
except FileNotFoundError:
76+
# Add a specific error for this common configuration issue
77+
logging.error(
78+
f"Critical Error: Private key file not found at {PRIVATE_KEY_FILE}")
79+
logging.error(
80+
"Please update the PRIVATE_KEY_FILE constant in this script.")
81+
sys.exit(1)
82+
except Exception as e:
83+
# Catch-all for other critical errors (e.g., network, auth failures)
84+
logging.error(f"A critical error occurred: {e}", exc_info=True)
85+
sys.exit(1)
86+
87+
logging.info("Device shut down gracefully. Exiting.")

0 commit comments

Comments
 (0)