Skip to content

feat: multi-tenant MQTT and strict device isolation#128

Merged
DaTiC0 merged 11 commits into
mainfrom
feat/mqtt-connectivity
May 5, 2026
Merged

feat: multi-tenant MQTT and strict device isolation#128
DaTiC0 merged 11 commits into
mainfrom
feat/mqtt-connectivity

Conversation

@DaTiC0

@DaTiC0 DaTiC0 commented May 4, 2026

Copy link
Copy Markdown
Owner

Summary

This PR introduces a significant architectural shift to support multi-tenancy and enhanced security:

  • Multi-tenant MQTT: MQTT topics and log buffers are now scoped by user_id, ensuring message isolation between users.
  • Strict Device Isolation: Devices are now stored and accessed via user-scoped paths in Firebase Realtime Database.
  • Dynamic MQTT Monitoring: The MQTT monitor page now displays live status and logs filtered by the authenticated user.
  • Production Hardening: Expanded password column capacity and added environment variable support for MQTT TLS and custom ports.
  • Documentation: Updated README.md with new features and environment variables.

Key Changes

  • Refactored action_devices.py to enforce user_id scoping for all SYNC and EXECUTE operations.
  • Updated routes.py to handle agentUserId and current_token for consistent user resolution.
  • Modernized notifications.py with thread-safe user-scoped log buffers.
  • Fixed OAuth test mocks in tests/test_app.py.
  • Added comprehensive multi-tenant tests in tests/test_multi_tenant.py.

Test plan

  • All existing tests pass (make test).
  • Verified MQTT multi-tenant routing with new test suite.
  • Manually verified OAuth bypass in test environment.

Production Note

While this PR significantly improves the foundation, a follow-up "Production Hardening" phase is recommended to address:

  • CSRF protection for forms.
  • Secure session cookie configuration.
  • Rate limiting on auth endpoints.

🤖 Generated with Gemini CLI

Summary by Sourcery

Introduce user-scoped device management and MQTT handling to support multi-tenancy and stricter per-user isolation across devices, logs, and smarthome requests.

New Features:

  • Scope Firebase device paths and SYNC/EXECUTE flows by user_id to isolate devices per account.
  • Add dashboard-specific device view data derived from live Firebase state for the Device Management UI.
  • Introduce user-aware MQTT topic structure and per-user log filtering for the MQTT monitor page.

Bug Fixes:

  • Ensure smarthome OAuth tests correctly mock tokens and agentUserId resolution for multi-tenant flows.

Enhancements:

  • Normalize and validate user and device identifiers to prevent path traversal and invalid Firebase paths.
  • Expand MQTT log buffer capacity and include user context to better support multi-tenant monitoring.
  • Resolve smarthome user scope consistently from agentUserId, authenticated users, or configured fallback.
  • Update sync, report_state, and devices routes to operate on the authenticated user's scope and improve dashboard status display.
  • Subscribe to new multi-tenant MQTT topics while keeping legacy topics for backward compatibility.

Documentation:

  • Document new MQTT configuration options, TLS support, and multi-tenant behavior in README.

Tests:

  • Add multi-tenant test suite covering scoped Firebase paths, MQTT topics, and per-user log filtering.
  • Extend existing app and notifications tests to validate user-scoped device dashboards and MQTT log rendering.

@sourcery-ai

sourcery-ai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements multi-tenant device and MQTT handling by scoping Firebase paths, MQTT topics, and logs to per-user identifiers, adds a dashboard-friendly device view, and wires routes and tests to enforce strict user isolation and dynamic monitoring.

Sequence diagram for multi-tenant smarthome intent handling

sequenceDiagram
    actor User
    participant GoogleAssistant as GoogleAssistant
    participant FlaskApp as FlaskApp
    participant Routes as routes_smarthome
    participant Actions as action_devices_actions
    participant Firebase as Firebase_RTD
    participant MQTT as MQTT_Broker

    User ->> GoogleAssistant: Send SYNC/QUERY/EXECUTE intent
    GoogleAssistant ->> FlaskApp: HTTP POST /smarthome (JSON)
    FlaskApp ->> Routes: smarthome()
    Routes ->> Routes: _resolve_smarthome_user_scope(req)
    note over Routes: Resolve user_scope and agent_user_id
    Routes ->> Actions: actions(req, user_id=user_scope, agent_user_id=agent_user_id)

    alt SYNC intent
        Actions ->> Actions: onSync(user_id, agent_user_id)
        Actions ->> Firebase: rsync(user_id)
        Firebase -->> Actions: user_scoped_devices
        Actions -->> Routes: SYNC payload (agentUserId, devices)
    else QUERY intent
        Actions ->> Actions: onQuery(body, user_id)
        loop for each device
            Actions ->> Actions: rquery(deviceId, user_id)
            Actions ->> Firebase: _get_scoped_device_states(deviceId, user_id)
            Firebase -->> Actions: states
        end
        Actions -->> Routes: QUERY payload
    else EXECUTE intent
        Actions ->> Actions: onExecute(body, user_id)
        loop for each execution
            Actions ->> Actions: commands(payload, deviceId, execCommand, params, user_id)
            Actions ->> Actions: rexecute(deviceId, params, user_id)
            Actions ->> Firebase: update user_scoped states
            Firebase -->> Actions: updated states
        end
        Actions -->> Routes: EXECUTE payload
        note over Actions,MQTT: Publish multi-tenant MQTT notification
        Actions ->> MQTT: publish(topic="user_id/deviceId/notification", payload=params)
    end

    Routes ->> FlaskApp: Build response {'requestId', 'payload'}
    FlaskApp -->> GoogleAssistant: JSON response
    GoogleAssistant -->> User: Present result
Loading

Sequence diagram for multi-tenant MQTT status handling and logging

sequenceDiagram
    participant Device as IoT_Device
    participant MQTT as MQTT_Broker
    participant FlaskMQTT as Flask_MQTT_Client
    participant Notifications as notifications_module
    participant Firebase as Firebase_RTD
    participant Routes as routes_mqtt_log
    participant Browser as Web_Browser

    Device ->> MQTT: PUBLISH {user_id}/{device_id}/status
    MQTT ->> FlaskMQTT: Deliver message
    FlaskMQTT ->> Notifications: handle_messages(message)

    Notifications ->> Notifications: _decode_payload(payload)
    Notifications ->> Notifications: Parse topic into user_id, device_id, msg_type
    alt msg_type is status
        Notifications ->> Firebase: db.reference(/users/user_id/devices/device_id/states).update(state_updates)
        Firebase -->> Notifications: ack
    end

    Notifications ->> Notifications: _append_mqtt_log(topic, payload, Received, user_id)

    User ->> Browser: Open /mqtt_log
    Browser ->> Routes: HTTP GET /mqtt_log
    Routes ->> Notifications: get_mqtt_logs(user_id)
    Notifications ->> Notifications: filter logs by user_id or None
    Notifications -->> Routes: filtered_log_entries
    Routes -->> Browser: Render mqtt_log.html with logs
    Browser ->> User: Show user_scoped MQTT log
Loading

Class diagram for updated multi-tenant helpers and MQTT logging

classDiagram
    class ActionDevices {
        +reference(user_id)
        +rstate(user_id)
        +rsync(user_id)
        +get_dashboard_devices(user_id)
        +rquery(deviceId, user_id)
        +rexecute(deviceId, parameters, user_id)
        +onSync(user_id, agent_user_id)
        +onQuery(body, user_id)
        +onExecute(body, user_id)
        +commands(payload, deviceId, execCommand, params, user_id)
        +actions(req, user_id, agent_user_id)
        +report_state(user_id)
        _normalize_user_scope(user_id)
        _to_sync_device(device_id, raw_data)
        _build_sync_devices(snapshot)
        _get_scoped_snapshot(user_id)
        _get_scoped_device_states(device_id, user_id)
        _normalize_device_type(device_type)
    }

    class RoutesModule {
        +sync_devices()
        +devices()
        +device_status(device_id)
        +mqtt_log()
        +smarthome()
        _resolve_smarthome_user_scope(req)
    }

    class NotificationsModule {
        +get_mqtt_logs(user_id)
        +handle_connect(client, userdata, flags, rc)
        +handle_disconnect(client, userdata, rc)
        +handle_messages(client, userdata, message)
        +handle_publish(client, userdata, mid)
        _append_mqtt_log(topic, payload, status, user_id)
        _decode_payload(payload)
    }

    class MQTTBroker {
        +publish(topic, payload, qos)
        +subscribe(topic)
    }

    class FirebaseRTDB {
        +reference(path)
        +get()
        +child(key)
        +update(data)
    }

    RoutesModule --> ActionDevices : uses
    RoutesModule --> NotificationsModule : uses

    ActionDevices --> FirebaseRTDB : reads_writes
    NotificationsModule --> FirebaseRTDB : updates_status_states
    NotificationsModule --> MQTTBroker : subscribes
    ActionDevices --> MQTTBroker : publishes_notifications

    class DeviceDashboardViewModel {
        +id
        +display_name
        +type
        +type_label
        +icon
        +is_online
        +status
        +status_class
    }

    ActionDevices --> DeviceDashboardViewModel : builds_list_for_devices_template
Loading

File-Level Changes

Change Details Files
Scope all Firebase device access and SYNC/EXECUTE helpers by user_id for strict per-user isolation.
  • Introduced helpers for normalizing user scope, building scoped snapshots, and constructing SYNC-compatible device lists.
  • Updated reference, rstate, rsync, rquery, rexecute, onSync, onQuery, onExecute, commands, actions, and report_state to accept user_id/agent_user_id and operate on /users/{user_id}/devices paths.
  • Added get_dashboard_devices and device-type normalization utilities for dashboard consumption and online/offline status rendering.
action_devices.py
Resolve smarthome user scope consistently and pass it through web routes for devices, MQTT logs, and sync/report operations.
  • Added _resolve_smarthome_user_scope to prioritize request agentUserId, then OAuth user, then session user, then AGENT_USER_ID.
  • Updated /sync, /devices, /device/, /mqtt_log, and /smarthome routes to require auth where appropriate and to call action_devices and notifications helpers with the resolved user scope.
  • Switched devices route to use get_dashboard_devices instead of onSync for richer device metadata.
routes.py
Make MQTT logging multi-tenant-aware and support user-scoped MQTT status updates into Firebase.
  • Expanded MQTT log buffer size and stored user_id per entry, with filtering in get_mqtt_logs(user_id).
  • Parsed MQTT topics as {user_id}/{device_id}/{notification
status}, updating Firebase device states for status messages under /users/{user_id}/devices/{device_id}/states.
  • Kept legacy topic subscriptions while subscribing to new multi-tenant topics in app initialization.
  • Align templates, tests, and documentation with multi-tenant behavior and new device/log structures.
    • Updated devices.html to use new dashboard device fields (display_name, icon, type_label, status, status_class).
    • Extended and fixed tests to mock OAuth tokens, validate user-scope resolution, dashboard routing, MQTT log behavior, and multi-tenant Firebase/MQTT paths.
    • Documented new MQTT/TLS env vars and multi-tenant features in README.
    templates/devices.html
    tests/test_app.py
    tests/test_notifications.py
    tests/test_multi_tenant.py
    README.md

    Tips and commands

    Interacting with Sourcery

    • Trigger a new review: Comment @sourcery-ai review on the pull request.
    • Continue discussions: Reply directly to Sourcery's review comments.
    • Generate a GitHub issue from a review comment: Ask Sourcery to create an
      issue from a review comment by replying to it. You can also reply to a
      review comment with @sourcery-ai issue to create an issue from it.
    • Generate a pull request title: Write @sourcery-ai anywhere in the pull
      request title to generate a title at any time. You can also comment
      @sourcery-ai title on the pull request to (re-)generate the title at any time.
    • Generate a pull request summary: Write @sourcery-ai summary anywhere in
      the pull request body to generate a PR summary at any time exactly where you
      want it. You can also comment @sourcery-ai summary on the pull request to
      (re-)generate the summary at any time.
    • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
      request to (re-)generate the reviewer's guide at any time.
    • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
      pull request to resolve all Sourcery comments. Useful if you've already
      addressed all the comments and don't want to see them anymore.
    • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
      request to dismiss all existing Sourcery reviews. Especially useful if you
      want to start fresh with a new review - don't forget to comment
      @sourcery-ai review to trigger a new review!

    Customizing Your Experience

    Access your dashboard to:

    • Enable or disable review features such as the Sourcery-generated pull request
      summary, the reviewer's guide, and others.
    • Change the review language.
    • Add, remove or edit custom review instructions.
    • Adjust other review settings.

    Getting Help

    @gemini-code-assist gemini-code-assist Bot left a comment

    Copy link
    Copy Markdown
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Code Review

    This pull request implements multi-tenancy support by scoping Firebase device data and MQTT communication by user ID. Key changes include updating Firebase references to use user-specific paths, modifying MQTT topics to a multi-tenant structure, and enhancing the smart home intent handler to resolve user scope from authentication tokens or request payloads. Feedback identifies performance improvements for the MQTT callback, the need for better validation and backward compatibility in MQTT message handling, and a potential issue with null user IDs during topic construction.

    Comment thread notifications.py Outdated
    Comment on lines +109 to +112
    from firebase_admin import db
    # Update status in /users/{user_id}/devices/{device_id}/states
    # Assuming payload is a JSON string of states
    import json

    Copy link
    Copy Markdown
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    medium

    Avoid importing modules inside a function that is called frequently, such as an MQTT message callback. These imports should be moved to the top of the file to improve performance and follow standard Python practices.

    Comment thread notifications.py Outdated
    Comment on lines +101 to +117
    if len(parts) >= 3:
    user_id = parts[0]
    device_id = parts[1]
    msg_type = parts[2]

    # Try to update Firebase if it's a status message
    if msg_type == 'status':
    try:
    from firebase_admin import db
    # Update status in /users/{user_id}/devices/{device_id}/states
    # Assuming payload is a JSON string of states
    import json
    state_updates = json.loads(payload)
    db.reference(f'/users/{user_id}/devices/{device_id}/states').update(state_updates)
    logger.debug("Updated Firebase status for %s/%s", user_id, device_id)
    except Exception as e:
    logger.error("Failed to update Firebase from MQTT: %s", e)

    Copy link
    Copy Markdown
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    medium

    The logic for updating Firebase from MQTT status messages has several issues:

    1. Compatibility: It only handles 3-part topics, breaking backward compatibility for legacy 2-part topics (device/status) which are still subscribed to in app.py.
    2. Robustness: It should verify that state_updates is a dictionary before calling .update() to prevent runtime errors if the payload is a simple JSON value (like a boolean or string).
    3. Sanitization: The user_id and device_id extracted from the topic should be validated/normalized before being used in a Firebase path to prevent potential path traversal if the MQTT broker is not strictly locked down.

    Comment thread action_devices.py Outdated
    params = payload['commands'][0]['states']
    mqtt.publish(topic=str(deviceId) + '/' + 'notification',
    # Multi-tenant MQTT topic: {user_id}/{device_id}/notification
    mqtt_topic = f"{user_id}/{deviceId}/notification"

    Copy link
    Copy Markdown
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    medium

    If user_id is None (e.g., for legacy users or when fallback is active), the MQTT topic will be constructed as None/{deviceId}/notification, which is likely incorrect. It should fall back to the legacy topic structure or use a default prefix to maintain compatibility.

    Suggested change
    mqtt_topic = f"{user_id}/{deviceId}/notification"
    mqtt_topic = f"{user_id}/{deviceId}/notification" if user_id else f"{deviceId}/notification"

    @sourcery-ai sourcery-ai Bot left a comment

    Copy link
    Copy Markdown
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Hey - I've found 3 issues, and left some high level feedback:

    • In actions the MQTT topic is built directly from user_id (f"{user_id}/{deviceId}/notification") without reusing _normalize_user_scope, which means a malicious or malformed agentUserId from the request could inject slashes into the topic hierarchy; consider normalizing or validating user_id before using it in MQTT topic names.
    • In notifications.handle_messages, the Firebase update path /users/{user_id}/devices/{device_id}/states is constructed inline instead of going through the new helper functions in action_devices (e.g., _normalize_user_scope / _get_scoped_device_states), which can lead to inconsistent path validation rules between MQTT-driven updates and HTTP-driven operations; consider centralizing this path construction/normalization.
    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Overall Comments
    - In `actions` the MQTT topic is built directly from `user_id` (`f"{user_id}/{deviceId}/notification"`) without reusing `_normalize_user_scope`, which means a malicious or malformed `agentUserId` from the request could inject slashes into the topic hierarchy; consider normalizing or validating `user_id` before using it in MQTT topic names.
    - In `notifications.handle_messages`, the Firebase update path `/users/{user_id}/devices/{device_id}/states` is constructed inline instead of going through the new helper functions in `action_devices` (e.g., `_normalize_user_scope` / `_get_scoped_device_states`), which can lead to inconsistent path validation rules between MQTT-driven updates and HTTP-driven operations; consider centralizing this path construction/normalization.
    
    ## Individual Comments
    
    ### Comment 1
    <location path="action_devices.py" line_range="379-380" />
    <code_context>
                             deviceId = payload['commands'][0]['ids'][0]
                             params = payload['commands'][0]['states']
    -                        mqtt.publish(topic=str(deviceId) + '/' + 'notification',
    +                        # Multi-tenant MQTT topic: {user_id}/{device_id}/notification
    +                        mqtt_topic = f"{user_id}/{deviceId}/notification"
    +                        mqtt.publish(topic=mqtt_topic,
                                          payload=str(params), qos=0)  # SENDING MQTT MESSAGE
    </code_context>
    <issue_to_address>
    **🚨 issue (security):** Normalize or sanitize user_id before embedding it in the MQTT topic to avoid malformed topics and mis-attribution.
    
    Because `user_id` can reach this code via `_resolve_smarthome_user_scope` without `_normalize_user_scope`, a client-controlled `agentUserId` with slashes (e.g. `foo/bar`) will produce an MQTT topic like `foo/bar/<deviceId>/notification`. `handle_messages` will then parse this as `user_id='foo'`, `device_id='bar'`, `msg_type='<deviceId>'`, misrouting messages across tenants. Please ensure `user_id` is normalized/sanitized for MQTT topics (e.g. via `_normalize_user_scope` or by enforcing a single topic segment) and fail early on invalid values.
    </issue_to_address>
    
    ### Comment 2
    <location path="notifications.py" line_range="106-117" />
    <code_context>
    +        # Try to update Firebase if it's a status message
    +        if msg_type == 'status':
    +            try:
    +                from firebase_admin import db
    +                # Update status in /users/{user_id}/devices/{device_id}/states
    +                # Assuming payload is a JSON string of states
    +                import json
    +                state_updates = json.loads(payload)
    +                db.reference(f'/users/{user_id}/devices/{device_id}/states').update(state_updates)
    +                logger.debug("Updated Firebase status for %s/%s", user_id, device_id)
    </code_context>
    <issue_to_address>
    **suggestion (bug_risk):** Guard JSON decoding to avoid noisy errors when payloads are already decoded or non-JSON.
    
    Since `payload` comes from `_decode_payload`, it may already be a Python object or a non-JSON string. Unconditionally calling `json.loads(payload)` will raise for non-JSON payloads, producing noisy "failed to update Firebase" logs and blocking legitimate updates. Consider only calling `json.loads` when `payload` is a `str`, and skipping the Firebase update when JSON decoding fails so expected non-JSON messages don’t look like errors.
    
    ```suggestion
            # Try to update Firebase if it's a status message
            if msg_type == 'status':
                # Guard JSON decoding to avoid noisy errors when payloads are already decoded or non-JSON
                state_updates = None
    
                if isinstance(payload, dict):
                    # Payload already decoded by _decode_payload
                    state_updates = payload
                elif isinstance(payload, str):
                    # Only attempt JSON decoding for string payloads
                    try:
                        import json
                        state_updates = json.loads(payload)
                    except (ValueError, TypeError):
                        # Non-JSON string payloads are expected for some messages; skip Firebase update quietly
                        logger.debug(
                            "Skipping Firebase status update for non-JSON payload on %s/%s",
                            user_id,
                            device_id,
                        )
                else:
                    # Non-dict, non-str payloads (e.g. bytes) are skipped to avoid noisy errors
                    logger.debug(
                        "Skipping Firebase status update for unsupported payload type %s on %s/%s",
                        type(payload).__name__,
                        user_id,
                        device_id,
                    )
    
                if state_updates is not None:
                    try:
                        from firebase_admin import db
                        # Update status in /users/{user_id}/devices/{device_id}/states
                        db.reference(f'/users/{user_id}/devices/{device_id}/states').update(state_updates)
                        logger.debug("Updated Firebase status for %s/%s", user_id, device_id)
                    except Exception as e:
                        logger.error("Failed to update Firebase from MQTT: %s", e)
    ```
    </issue_to_address>
    
    ### Comment 3
    <location path="tests/test_app.py" line_range="452-461" />
    <code_context>
    +    def test_smarthome_uses_agent_user_scope(self):
    </code_context>
    <issue_to_address>
    **nitpick (testing):** Clarify and strengthen assertions around user scope vs agentUserId precedence in smarthome tests
    
    The test correctly verifies that `_resolve_smarthome_user_scope` prefers request-level `agentUserId` over `current_token.user.id`, but the first inline comment is misleading. Please update it to state that `agentUserId` takes precedence and `current_token.user.id` is a fallback.
    
    Also consider adding a complementary test where `agentUserId` is omitted from the payload, to assert that both `user_id` and `agent_user_id` then come from `current_token.user.id`. This will exercise both the primary and fallback resolution paths.
    </issue_to_address>

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    Comment thread action_devices.py Outdated
    Comment thread notifications.py Outdated
    Comment thread tests/test_app.py
    Comment on lines +452 to +461
    def test_smarthome_uses_agent_user_scope(self):
    payload = {
    'requestId': '1',
    'agentUserId': '42',
    'inputs': [{'intent': 'action.devices.SYNC', 'payload': {}}],
    }

    mock_token = MagicMock()
    mock_token.user.id = 1

    Copy link
    Copy Markdown
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    nitpick (testing): Clarify and strengthen assertions around user scope vs agentUserId precedence in smarthome tests

    The test correctly verifies that _resolve_smarthome_user_scope prefers request-level agentUserId over current_token.user.id, but the first inline comment is misleading. Please update it to state that agentUserId takes precedence and current_token.user.id is a fallback.

    Also consider adding a complementary test where agentUserId is omitted from the payload, to assert that both user_id and agent_user_id then come from current_token.user.id. This will exercise both the primary and fallback resolution paths.

    DaTiC0 added a commit that referenced this pull request May 4, 2026
    @DaTiC0

    DaTiC0 commented May 4, 2026

    Copy link
    Copy Markdown
    Owner Author

    I have addressed the feedback from the code reviews with the following fixes:

    🔐 Security & Consistency

    • MQTT Topic Sanitization: Normalized user_id using _normalize_user_scope before topic construction in action_devices.py. This prevents agentUserId injection attacks.
    • Centralized Firebase Logic: Introduced _get_user_device_states_ref in action_devices.py to centralize user-scoped path construction, ensuring consistency between HTTP and MQTT updates.

    🛠️ Robustness

    • Hardened MQTT Status Updates: Added guards to notifications.py for JSON decoding. It now safely handles already-decoded dicts, stringified JSON, and non-JSON payloads without raising noisy errors.
    • Type Safety: Improved device ID sanitization by enforcing string conversion and path-traversal checks.

    🧪 Testing

    • New Test Case: Added test_smarthome_fallback_to_token_user_id to verify user resolution when agentUserId is omitted.
    • Improved Coverage: Updated multi-tenant tests to align with the new centralized helpers.

    All 43 tests are passing (make test). Ready for final review!

    🤖 Resolved via Gemini CLI

    @DaTiC0
    DaTiC0 force-pushed the feat/mqtt-connectivity branch from 469452f to ed6a7a4 Compare May 5, 2026 05:48
    @DaTiC0
    DaTiC0 merged commit 3b72bc5 into main May 5, 2026
    5 of 6 checks passed
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    None yet

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    2 participants